From e62122dcb222999de344517a3be9f4c03016f9c9 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sat, 28 Jun 2014 12:34:39 -0700 Subject: [PATCH 001/399] Add new growing smart allocator to consolidate code. Refactor 304 responses and cached responses in http response cache. --- AndroidAsync/AndroidAsync.iml | 3 + .../async/AsyncNetworkSocket.java | 12 +- .../async/http/ResponseCacheMiddleware.java | 133 +++++------------- .../koushikdutta/async/util/Allocator.java | 38 +++++ AndroidAsyncTest/AndroidAsyncTest.iml | 3 + 5 files changed, 88 insertions(+), 101 deletions(-) create mode 100644 AndroidAsync/src/com/koushikdutta/async/util/Allocator.java diff --git a/AndroidAsync/AndroidAsync.iml b/AndroidAsync/AndroidAsync.iml index 8c69f527d..edb7738b9 100644 --- a/AndroidAsync/AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync.iml @@ -4,6 +4,9 @@ - - + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -51,9 +57,7 @@ - - diff --git a/AndroidAsync/build.gradle b/AndroidAsync/build.gradle index 34e2ebc8c..1f19ca7c3 100644 --- a/AndroidAsync/build.gradle +++ b/AndroidAsync/build.gradle @@ -8,9 +8,6 @@ buildscript { } apply plugin: 'com.android.library' -dependencies { -} - android { sourceSets { main { @@ -25,14 +22,6 @@ android { compileSdkVersion 19 buildToolsVersion "20.0.0" - - android { - lintOptions { - abortOnError false - } - } - - publishNonDefault true } // upload to maven task diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index cfc6a10bd..504d5c602 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -1,6 +1,8 @@ package com.koushikdutta.async.http; +import android.annotation.SuppressLint; import android.net.Uri; +import android.os.Build; import android.text.TextUtils; import com.koushikdutta.async.AsyncSSLException; @@ -36,9 +38,14 @@ import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; import java.net.URI; import java.net.URL; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeoutException; public class AsyncHttpClient { @@ -67,6 +74,36 @@ public AsyncHttpClient(AsyncServer server) { insertMiddleware(sslSocketMiddleware = new AsyncSSLSocketMiddleware(this)); } + + @SuppressLint("NewApi") + private static void setupAndroidProxy(AsyncHttpRequest request) { + // using a explicit proxy? + if (request.proxyHost != null) + return; + + List proxies = ProxySelector.getDefault().select(URI.create(request.getUri().toString())); + if (proxies.isEmpty()) + return; + Proxy proxy = proxies.get(0); + if (proxy.type() != Proxy.Type.HTTP) + return; + if (!(proxy.address() instanceof InetSocketAddress)) + return; + InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); + String proxyHost; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + proxyHost = proxyAddress.getHostString(); + } + else { + InetAddress address = proxyAddress.getAddress(); + if (address!=null) + proxyHost = address.getHostAddress(); + else + proxyHost = proxyAddress.getHostName(); + } + request.enableProxy(proxyHost, proxyAddress.getPort()); + } + public AsyncSocketMiddleware getSocketMiddleware() { return socketMiddleware; } @@ -278,7 +315,7 @@ public void setDataEmitter(DataEmitter emitter) { newReq.LOGTAG = request.LOGTAG; newReq.proxyHost = request.proxyHost; newReq.proxyPort = request.proxyPort; - newReq.useAndroidProxy = request.useAndroidProxy; + setupAndroidProxy(newReq); copyHeader(request, newReq, "User-Agent"); copyHeader(request, newReq, "Range"); request.logi("Redirecting"); @@ -373,6 +410,9 @@ public AsyncSocket detachSocket() { } }; + // set up the system default proxy and connect + setupAndroidProxy(request); + synchronized (mMiddleware) { for (AsyncHttpClientMiddleware middleware: mMiddleware) { Cancellable socketCancellable = middleware.getSocket(data); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java index dc1ef13c2..22316b57f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java @@ -1,8 +1,6 @@ package com.koushikdutta.async.http; -import android.annotation.SuppressLint; import android.net.Uri; -import android.os.Build; import android.util.Log; import com.koushikdutta.async.AsyncSSLException; @@ -18,11 +16,6 @@ import org.apache.http.message.BasicHeader; import org.apache.http.params.HttpParams; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.ProxySelector; -import java.net.URI; import java.util.List; import java.util.Map; @@ -324,61 +317,21 @@ public AsyncHttpRequest addHeader(String name, String value) { String proxyHost; int proxyPort = -1; - boolean useAndroidProxy = true; public void enableProxy(String host, int port) { proxyHost = host; proxyPort = port; - useAndroidProxy = proxyPort == 0; - } - - public void enableSystemProxy(boolean enable) { - useAndroidProxy = enable; } public void disableProxy() { proxyHost = null; proxyPort = -1; - useAndroidProxy = false; - } - - @SuppressLint("NewApi") - private void setupAndroidProxy() { - List proxies = ProxySelector.getDefault().select(URI.create(getUri().toString())); - if (proxies.isEmpty()) { - disableProxy(); - } else { - Proxy proxy = proxies.get(0); - if (proxy.type() == Proxy.Type.DIRECT) { - disableProxy(); - } else if (proxy.type() == Proxy.Type.HTTP && proxy.address() instanceof InetSocketAddress) { - InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) - proxyHost = proxyAddress.getHostString(); - else { - InetAddress address = proxyAddress.getAddress(); - if (address!=null) - proxyHost = address.getHostAddress(); - else - proxyHost = proxyAddress.getHostName(); - } - proxyPort = proxyAddress.getPort(); - } - } } public String getProxyHost() { - if (useAndroidProxy) { - setupAndroidProxy(); - useAndroidProxy = false; - } return proxyHost; } public int getProxyPort() { - if (useAndroidProxy) { - setupAndroidProxy(); - useAndroidProxy = false; - } return proxyPort; } From d8a5060ca43c7efc5ff7b2b253791805cb928114 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sun, 13 Jul 2014 18:06:22 -0700 Subject: [PATCH 024/399] AsyncSSLSocketWrapper: Perform handshake before allowing read/write. --- .../async/AsyncSSLSocketWrapper.java | 207 +++++------------- .../async/http/AsyncSSLSocketMiddleware.java | 22 +- .../async/http/server/AsyncHttpServer.java | 14 +- 3 files changed, 85 insertions(+), 158 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 93c2865ad..bb6a16731 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -28,129 +28,28 @@ import javax.net.ssl.X509TrustManager; public class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket { + public interface HandshakeCallback { + public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket); + } + static SSLContext defaultSSLContext; AsyncSocket mSocket; BufferedDataEmitter mEmitter; BufferedDataSink mSink; - boolean mUnwrapping = false; + boolean mUnwrapping; + SSLEngine engine; + boolean finishedHandshake; + private int mPort; + private String mHost; + private boolean mWrapping; HostnameVerifier hostnameVerifier; - - /* - private static void initTLS_1_2() { - try { - defaultSSLContext = SSLContext.getInstance("TLSv1.2"); - } - catch (NoSuchAlgorithmException e) { - } - } - - private static void initTLS_1_1() { - try { - defaultSSLContext = SSLContext.getInstance("TLSv1.1"); - } - catch (NoSuchAlgorithmException e) { - } - } - - private static void initTLS() { - try { - defaultSSLContext = SSLContext.getInstance("TLS"); - } - catch (NoSuchAlgorithmException e) { - } - } - - static { - try { - initTLS_1_2(); - if (defaultSSLContext == null) - initTLS_1_1(); - if (defaultSSLContext == null) - initTLS(); - if (defaultSSLContext == null) - defaultSSLContext = SSLContext.getInstance("SSL"); - // critical extension 2.5.29.15 is implemented improperly prior to 4.0.3. - // https://code.google.com/p/android/issues/detail?id=9307 - // https://groups.google.com/forum/?fromgroups=#!topic/netty/UCfqPPk5O4s - // certs that use this extension will throw in Cipher.java. - // fallback is to use a custom SSLContext, and hack around the x509 extension. - TrustManager[] trustManagers = null; - if (Build.VERSION.SDK_INT <= 15) { - trustManagers = new TrustManager[] { new X509TrustManager() { - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - - public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) { - } - - public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { - for (X509Certificate cert : certs) { - if (cert != null && cert.getCriticalExtensionOIDs() != null) - cert.getCriticalExtensionOIDs().remove("2.5.29.15"); - } - } - } }; - } - defaultSSLContext.init(null, trustManagers, null); - } - catch (Exception ex) { - ex.printStackTrace(); - } - } - - // android SSL cipher suites were downgraded (!!) for some derpy reason. - // Paranoid people would be wise to enable the original/secure suites. - // http://op-co.de/blog/posts/android_ssl_downgrade/ - public static final String RECOMMENDED_CIPHERS[] = { - "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", - "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", - "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", - "TLS_DHE_RSA_WITH_AES_256_CBC_SHA", - "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", - "TLS_ECDHE_RSA_WITH_RC4_128_SHA", - "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", - "TLS_RSA_WITH_AES_128_CBC_SHA", - "TLS_RSA_WITH_AES_256_CBC_SHA", - "SSL_RSA_WITH_3DES_EDE_CBC_SHA", - "SSL_RSA_WITH_RC4_128_SHA", - "SSL_RSA_WITH_RC4_128_MD5", - "TLS_RSA_WITH_AES_256_CBC_SHA256", - }; - - public static final String RECOMMENDED_PROTOCOLS[] = { - "TLSv1" - }; - - public static void setupRecommendedEngineSecurity(SSLEngine engine) { - LinkedHashSet ciphers = new LinkedHashSet(Arrays.asList(engine.getSupportedCipherSuites())); - ciphers.addAll(Arrays.asList(engine.getSupportedCipherSuites())); - LinkedHashSet protocols = new LinkedHashSet(); - protocols.addAll(Arrays.asList(engine.getSupportedProtocols())); - - ArrayList enabledCiphers = new ArrayList(); - for (String cipher: RECOMMENDED_CIPHERS) { - if (ciphers.contains(cipher)) - enabledCiphers.add(cipher); - } - - ArrayList enabledProtocols = new ArrayList(); - for (String protocol: RECOMMENDED_PROTOCOLS) { - if (protocols.contains(protocol)) - enabledProtocols.add(protocol); - } - - enabledCiphers.addAll(Arrays.asList(engine.getEnabledCipherSuites())); - enabledProtocols.addAll(Arrays.asList(engine.getEnabledProtocols())); -// engine.setEnabledCipherSuites(enabledCiphers.toArray(new String[enabledCiphers.size()])); -// engine.setEnabledProtocols(enabledProtocols.toArray(new String[enabledProtocols.size()])); -// engine.setEnabledCipherSuites(RECOMMENDED_CIPHERS); - engine.setEnabledProtocols(new String[] {"SSL"}); - } - */ + HandshakeCallback handshakeCallback; + X509Certificate[] peerCertificates; + WritableCallback mWriteableCallback; + DataCallback mDataCallback; + TrustManager[] trustManagers; + boolean clientMode; static { // following is the "trust the system" certs setup @@ -195,19 +94,25 @@ public static SSLEngine createDefaultSSLEngine() { return defaultSSLContext.createSSLEngine(); } - @Override - public void end() { - mSocket.end(); - } - - public AsyncSSLSocketWrapper(AsyncSocket socket, String host, int port) { - this(socket, host, port, createDefaultSSLEngine(), null, null, true); + public static void handshake(AsyncSocket socket, + String host, int port, + SSLEngine sslEngine, + TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode, + HandshakeCallback callback) { + AsyncSSLSocketWrapper wrapper = new AsyncSSLSocketWrapper(socket, host, port, sslEngine, trustManagers, verifier, clientMode); + wrapper.handshakeCallback = callback; + try { + wrapper.engine.beginHandshake(); + wrapper.handleHandshakeStatus(wrapper.engine.getHandshakeStatus()); + } catch (SSLException e) { + wrapper.report(e); + } } - TrustManager[] trustManagers; - boolean clientMode; - - public AsyncSSLSocketWrapper(AsyncSocket socket, String host, int port, SSLEngine sslEngine, TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode) { + private AsyncSSLSocketWrapper(AsyncSocket socket, + String host, int port, + SSLEngine sslEngine, + TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode) { mSocket = socket; hostnameVerifier = verifier; this.clientMode = clientMode; @@ -277,7 +182,7 @@ else if (res.getStatus() == Status.BUFFER_UNDERFLOW) { bb.addFirst(b); b = ByteBufferList.EMPTY_BYTEBUFFER; } - handleResult(res); + handleHandshakeStatus(res.getHandshakeStatus()); if (b.remaining() == remaining && before == transformed.remaining()) { bb.addFirst(b); break; @@ -308,32 +213,30 @@ void addToPending(ByteBufferList out, ByteBuffer mReadTmp) { } - SSLEngine engine; - boolean finishedHandshake = false; - - private String mHost; + @Override + public void end() { + mSocket.end(); + } public String getHost() { return mHost; } - private int mPort; - public int getPort() { return mPort; } - private void handleResult(SSLEngineResult res) { - if (res.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + private void handleHandshakeStatus(HandshakeStatus status) { + if (status == HandshakeStatus.NEED_TASK) { final Runnable task = engine.getDelegatedTask(); task.run(); } - if (res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) { + if (status == HandshakeStatus.NEED_WRAP) { write(ByteBufferList.EMPTY_BYTEBUFFER); } - if (res.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) { + if (status == HandshakeStatus.NEED_UNWRAP) { mEmitter.onDataAvailable(); } @@ -380,6 +283,11 @@ private void handleResult(SSLEngineResult res) { throw e; } } + else { + finishedHandshake = true; + } + handshakeCallback.onHandshakeCompleted(null, this); + handshakeCallback = null; if (mWriteableCallback != null) mWriteableCallback.onWriteable(); mEmitter.onDataAvailable(); @@ -403,7 +311,6 @@ private void writeTmp(ByteBuffer mWriteTmp) { assert !mWriteTmp.hasRemaining(); } - private boolean mWrapping = false; int calculateAlloc(int remaining) { // alloc 50% more than we need for writing @@ -445,7 +352,7 @@ public void write(ByteBuffer bb) { else { mWriteTmp = ByteBufferList.obtain(calculateAlloc(bb.remaining())); } - handleResult(res); + handleHandshakeStatus(res.getHandshakeStatus()); } catch (SSLException e) { report(e); @@ -489,7 +396,7 @@ public void write(ByteBufferList bb) { } else { mWriteTmp = ByteBufferList.obtain(calculateAlloc(bb.remaining())); - handleResult(res); + handleHandshakeStatus(res.getHandshakeStatus()); } } catch (SSLException e) { @@ -501,8 +408,6 @@ public void write(ByteBufferList bb) { mWrapping = false; } - WritableCallback mWriteableCallback; - @Override public void setWriteableCallback(WritableCallback handler) { mWriteableCallback = handler; @@ -514,13 +419,21 @@ public WritableCallback getWriteableCallback() { } private void report(Exception e) { + final HandshakeCallback hs = handshakeCallback; + if (hs != null) { + handshakeCallback = null; + mSocket.setDataCallback(new NullDataCallback()); + mSocket.end(); + mSocket.close(); + hs.onHandshakeCompleted(e, null); + return; + } + CompletedCallback cb = getEndCallback(); if (cb != null) cb.onCompleted(e); } - DataCallback mDataCallback; - @Override public void setDataCallback(DataCallback callback) { mDataCallback = callback; @@ -596,8 +509,6 @@ public DataEmitter getDataEmitter() { return mSocket; } - X509Certificate[] peerCertificates; - @Override public X509Certificate[] getPeerCertificates() { return peerCertificates; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java index 4f3fff7df..868afc6fe 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java @@ -4,6 +4,7 @@ import android.os.Build; import android.text.TextUtils; +import com.koushikdutta.async.AsyncSSLSocket; import com.koushikdutta.async.AsyncSSLSocketWrapper; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.LineEmitter; @@ -70,6 +71,17 @@ protected SSLEngine createConfiguredSSLEngine(String host, int port) { return sslEngine; } + protected void tryHandshake(final ConnectCallback callback, AsyncSocket socket, final Uri uri, final int port) { + AsyncSSLSocketWrapper.handshake(socket, uri.getHost(), port, + createConfiguredSSLEngine(uri.getHost(), port), + trustManagers, hostnameVerifier, true, new AsyncSSLSocketWrapper.HandshakeCallback() { + @Override + public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { + callback.onConnectCompleted(e, socket); + } + }); + } + @Override protected ConnectCallback wrapCallback(final ConnectCallback callback, final Uri uri, final int port, final boolean proxied) { return new ConnectCallback() { @@ -77,10 +89,7 @@ protected ConnectCallback wrapCallback(final ConnectCallback callback, final Uri public void onConnectCompleted(Exception ex, final AsyncSocket socket) { if (ex == null) { if (!proxied) { - callback.onConnectCompleted(null, - new AsyncSSLSocketWrapper(socket, uri.getHost(), port, - createConfiguredSSLEngine(uri.getHost(), port), - trustManagers, hostnameVerifier, true)); + tryHandshake(callback, socket, uri, port); } else { // this SSL connection is proxied, must issue a CONNECT request to the proxy server @@ -112,10 +121,7 @@ public void onStringAvailable(String s) { socket.setDataCallback(null); socket.setEndCallback(null); if (TextUtils.isEmpty(s.trim())) { - callback.onConnectCompleted(null, - new AsyncSSLSocketWrapper(socket, uri.getHost(), port, - createConfiguredSSLEngine(uri.getHost(), port), - trustManagers, hostnameVerifier, true)); + tryHandshake(callback, socket, uri, port); } else { callback.onConnectCompleted(new IOException("unknown second status line"), socket); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java index 8d139ef4a..b1b393d64 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java @@ -1,9 +1,12 @@ package com.koushikdutta.async.http.server; +import android.annotation.TargetApi; import android.content.Context; import android.content.res.AssetManager; +import android.os.Build; import android.text.TextUtils; +import com.koushikdutta.async.AsyncSSLSocket; import com.koushikdutta.async.AsyncSSLSocketWrapper; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncServerSocket; @@ -39,6 +42,7 @@ import javax.net.ssl.SSLContext; +@TargetApi(Build.VERSION_CODES.ECLAIR) public class AsyncHttpServer { ArrayList mListeners = new ArrayList(); public void stop() { @@ -223,8 +227,14 @@ public void listenSecure(final int port, final SSLContext sslContext) { AsyncServer.getDefault().listen(null, port, new ListenCallback() { @Override public void onAccepted(AsyncSocket socket) { - AsyncSSLSocketWrapper sslSocket = new AsyncSSLSocketWrapper(socket, null, port, sslContext.createSSLEngine(), null, null, false); - mListenCallback.onAccepted(sslSocket); + AsyncSSLSocketWrapper.handshake(socket, null, port, sslContext.createSSLEngine(), null, null, false, + new AsyncSSLSocketWrapper.HandshakeCallback() { + @Override + public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { + if (socket != null) + mListenCallback.onAccepted(socket); + } + }); } @Override From 9473c0e25904c1dc2034d835ed770dd9c46639fd Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 16 Jul 2014 21:33:55 -0700 Subject: [PATCH 025/399] ByteBuffer List fixes: Stop assuming everything is array backed. readString/peekString refactor. Default charset is ascii. SSL: Conscrypt an SPDY prep. --- .gitignore | 1 + AndroidAsync/AndroidAsync-AndroidAsync.iml | 6 + AndroidAsync/build.gradle | 21 +++- .../koushikdutta/async/AsyncSSLSocket.java | 3 + .../async/AsyncSSLSocketWrapper.java | 17 ++- .../koushikdutta/async/ByteBufferList.java | 109 +++++++++--------- .../com/koushikdutta/async/ZipDataSink.java | 2 +- .../async/http/AsyncSSLSocketMiddleware.java | 31 ++--- .../async/http/ResponseCacheMiddleware.java | 9 +- .../async/parser/StringParser.java | 5 +- .../koushikdutta/async/util/Allocator.java | 3 +- .../koushikdutta/async/test/ParserTests.java | 5 + 12 files changed, 134 insertions(+), 78 deletions(-) diff --git a/.gitignore b/.gitignore index 1b27a2363..b9a860d79 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build .DS_Store okhttp +okio diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index 1cdbd092c..f88736b56 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -60,6 +60,12 @@ + + + + + + diff --git a/AndroidAsync/build.gradle b/AndroidAsync/build.gradle index 1f19ca7c3..38c3f67e6 100644 --- a/AndroidAsync/build.gradle +++ b/AndroidAsync/build.gradle @@ -9,17 +9,36 @@ buildscript { apply plugin: 'com.android.library' android { + dependencies { +// compile 'com.squareup.okio:okio:+' +// androidTestCompile 'com.squareup.okhttp:okhttp:1.+' + } + sourceSets { main { manifest.srcFile 'AndroidManifest.xml' - java.srcDirs=['src/'] + jniLibs.srcDirs = ['libs/'] + + java.srcDirs=['src/' +// , 'okhttp/' +// , 'okhttp-shim/' +// , '../okio/okio/src/main/java/' + , '../conscrypt/' + , '../compat/' +// , '../okhttp/okhttp/src/main/java/' + ] } androidTest.java.srcDirs=['test/src/'] androidTest.res.srcDirs=['test/res/'] androidTest.assets.srcDirs=['test/assets/'] } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + compileSdkVersion 19 buildToolsVersion "20.0.0" } diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocket.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocket.java index e45d9c0a8..ba52642b5 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocket.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocket.java @@ -2,6 +2,9 @@ import java.security.cert.X509Certificate; +import javax.net.ssl.SSLEngine; + public interface AsyncSSLSocket extends AsyncSocket { public X509Certificate[] getPeerCertificates(); + public SSLEngine getSSLEngine(); } diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index bb6a16731..36d087c51 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -90,17 +90,23 @@ public void checkServerTrusted(java.security.cert.X509Certificate[] certs, Strin } } - public static SSLEngine createDefaultSSLEngine() { - return defaultSSLContext.createSSLEngine(); + public static SSLContext getDefaultSSLContext() { + return defaultSSLContext; } public static void handshake(AsyncSocket socket, String host, int port, SSLEngine sslEngine, TrustManager[] trustManagers, HostnameVerifier verifier, boolean clientMode, - HandshakeCallback callback) { + final HandshakeCallback callback) { AsyncSSLSocketWrapper wrapper = new AsyncSSLSocketWrapper(socket, host, port, sslEngine, trustManagers, verifier, clientMode); wrapper.handshakeCallback = callback; + socket.setClosedCallback(new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + callback.onHandshakeCompleted(new SSLException(ex), null); + } + }); try { wrapper.engine.beginHandshake(); wrapper.handleHandshakeStatus(wrapper.engine.getHandshakeStatus()); @@ -202,6 +208,11 @@ else if (res.getStatus() == Status.BUFFER_UNDERFLOW) { }); } + @Override + public SSLEngine getSSLEngine() { + return engine; + } + void addToPending(ByteBufferList out, ByteBuffer mReadTmp) { mReadTmp.flip(); if (mReadTmp.hasRemaining()) { diff --git a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java index e39ff5a28..c036bdf84 100644 --- a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java +++ b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java @@ -6,6 +6,8 @@ import com.koushikdutta.async.util.Charsets; +import java.io.IOException; +import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.Charset; @@ -47,9 +49,12 @@ public void addAll(ByteBuffer... bb) { public byte[] getAllByteArray() { // fast path to return the contents of the first and only byte buffer, // if that's what we're looking for. avoids allocation. - if (mBuffers.size() == 1 && mBuffers.peek().capacity() == remaining()) { - remaining = 0; - return mBuffers.remove().array(); + if (mBuffers.size() == 1) { + ByteBuffer peek = mBuffers.peek(); + if (peek.capacity() == remaining() && peek.isDirect()) { + remaining = 0; + return mBuffers.remove().array(); + } } byte[] ret = new byte[remaining()]; @@ -206,46 +211,7 @@ private ByteBuffer read(int count) { return first.order(order); } - ByteBuffer ret = null; - int retOffset = 0; - int allocSize = 0; - - // attempt to find a buffer that can fit this, and the necessary - // alloc size to not leave anything leftover in the final buffer. - for (ByteBuffer b: mBuffers) { - if (allocSize >= count) - break; - // see if this fits... - if ((ret == null || b.capacity() > ret.capacity()) && b.capacity() >= count) { - ret = b; - retOffset = allocSize; - } - allocSize += b.remaining(); - } - - if (ret != null && ret.capacity() > allocSize) { - // move the current contents of the target bytebuffer around to its final position - System.arraycopy(ret.array(), ret.arrayOffset() + ret.position(), ret.array(), ret.arrayOffset() + retOffset, ret.remaining()); - int retRemaining = ret.remaining(); - ret.position(0); - ret.limit(allocSize); - allocSize = 0; - while (allocSize < count) { - ByteBuffer b = mBuffers.remove(); - if (b != ret) { - System.arraycopy(b.array(), b.arrayOffset() + b.position(), ret.array(), ret.arrayOffset() + allocSize, b.remaining()); - allocSize += b.remaining(); - reclaim(b); - } - else { - allocSize += retRemaining; - } - } - mBuffers.addFirst(ret); - return ret.order(order); - } - - ret = obtain(count); + ByteBuffer ret = obtain(count); ret.limit(count); byte[] bytes = ret.array(); int offset = 0; @@ -349,30 +315,43 @@ public void spewString() { System.out.println(peekString()); } - // not doing toString as this is really nasty in the debugger... public String peekString() { + return peekString(null); + } + + // not doing toString as this is really nasty in the debugger... + public String peekString(Charset charset) { + if (charset == null) + charset = Charsets.US_ASCII; StringBuilder builder = new StringBuilder(); for (ByteBuffer bb: mBuffers) { - builder.append(new String(bb.array(), bb.arrayOffset() + bb.position(), bb.remaining())); + byte[] bytes; + int offset; + int length; + if (bb.isDirect()) { + bytes = new byte[bb.remaining()]; + offset = 0; + length = bb.remaining(); + bb.get(bytes); + } + else { + bytes = bb.array(); + offset = bb.arrayOffset() + bb.position(); + length = bb.remaining(); + } + builder.append(new String(bytes, offset, length, charset)); } return builder.toString(); } public String readString() { - return readString(Charsets.US_ASCII); + return readString(null); } public String readString(Charset charset) { - if (charset == null) - charset = Charset.defaultCharset(); - StringBuilder builder = new StringBuilder(); - while (mBuffers.size() > 0) { - ByteBuffer bb = mBuffers.remove(); - builder.append(new String(bb.array(), bb.arrayOffset() + bb.position(), bb.remaining(), charset)); - reclaim(bb); - } - remaining = 0; - return builder.toString(); + String ret = peekString(charset); + recycle(); + return ret; } static class Reclaimer implements Comparator { @@ -512,4 +491,22 @@ public static void obtainArray(ByteBuffer[] arr, int size) { } public static final ByteBuffer EMPTY_BYTEBUFFER = ByteBuffer.allocate(0); + + public static void writeOutputStream(OutputStream out, ByteBuffer b) throws IOException { + byte[] bytes; + int offset; + int length; + if (b.isDirect()) { + bytes = new byte[b.remaining()]; + offset = 0; + length = b.remaining(); + b.get(bytes); + } + else { + bytes = b.array(); + offset = b.arrayOffset() + b.position(); + length = b.remaining(); + } + out.write(bytes, offset, length); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/ZipDataSink.java b/AndroidAsync/src/com/koushikdutta/async/ZipDataSink.java index 4fb6498fa..6838590f2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/ZipDataSink.java +++ b/AndroidAsync/src/com/koushikdutta/async/ZipDataSink.java @@ -50,7 +50,7 @@ public ByteBufferList filter(ByteBufferList bb) { if (bb != null) { while (bb.size() > 0) { ByteBuffer b = bb.remove(); - zop.write(b.array(), b.arrayOffset() + b.position(), b.remaining()); + ByteBufferList.writeOutputStream(zop, b); ByteBufferList.reclaim(b); } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java index 868afc6fe..b339c935e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java @@ -1,7 +1,6 @@ package com.koushikdutta.async.http; import android.net.Uri; -import android.os.Build; import android.text.TextUtils; import com.koushikdutta.async.AsyncSSLSocket; @@ -14,7 +13,6 @@ import com.koushikdutta.async.http.libcore.RawHeaders; import java.io.IOException; -import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; @@ -22,7 +20,6 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; public class AsyncSSLSocketMiddleware extends AsyncSocketMiddleware { public AsyncSSLSocketMiddleware(AsyncHttpClient client) { @@ -35,6 +32,10 @@ public void setSSLContext(SSLContext sslContext) { this.sslContext = sslContext; } + public SSLContext getSSLContext() { + return sslContext != null ? sslContext : AsyncSSLSocketWrapper.getDefaultSSLContext(); + } + TrustManager[] trustManagers; public void setTrustManagers(TrustManager[] trustManagers) { @@ -47,7 +48,7 @@ public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { this.hostnameVerifier = hostnameVerifier; } - List engineConfigurators = new ArrayList(); + protected List engineConfigurators = new ArrayList(); public void addEngineConfigurator(AsyncSSLEngineConfigurator engineConfigurator) { engineConfigurators.add(engineConfigurator); @@ -58,11 +59,8 @@ public void clearEngineConfigurators() { } protected SSLEngine createConfiguredSSLEngine(String host, int port) { - SSLEngine sslEngine; - if (sslContext != null) - sslEngine = sslContext.createSSLEngine(); - else - sslEngine = AsyncSSLSocketWrapper.createDefaultSSLEngine(); + SSLContext sslContext = getSSLContext(); + SSLEngine sslEngine = sslContext.createSSLEngine(); for (AsyncSSLEngineConfigurator configurator : engineConfigurators) { configurator.configureEngine(sslEngine, host, port); @@ -71,15 +69,20 @@ protected SSLEngine createConfiguredSSLEngine(String host, int port) { return sslEngine; } - protected void tryHandshake(final ConnectCallback callback, AsyncSocket socket, final Uri uri, final int port) { - AsyncSSLSocketWrapper.handshake(socket, uri.getHost(), port, - createConfiguredSSLEngine(uri.getHost(), port), - trustManagers, hostnameVerifier, true, new AsyncSSLSocketWrapper.HandshakeCallback() { + protected AsyncSSLSocketWrapper.HandshakeCallback createHandshakeCallback(final ConnectCallback callback) { + return new AsyncSSLSocketWrapper.HandshakeCallback() { @Override public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { callback.onConnectCompleted(e, socket); } - }); + }; + } + + protected void tryHandshake(final ConnectCallback callback, AsyncSocket socket, final Uri uri, final int port) { + AsyncSSLSocketWrapper.handshake(socket, uri.getHost(), port, + createConfiguredSSLEngine(uri.getHost(), port), + trustManagers, hostnameVerifier, true, + createHandshakeCallback(callback)); } @Override diff --git a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java index afb4f9ab4..851d77dd8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java @@ -43,6 +43,8 @@ import java.util.List; import java.util.Map; +import javax.net.ssl.SSLEngine; + public class ResponseCacheMiddleware extends SimpleMiddleware { public static final int ENTRY_METADATA = 0; public static final int ENTRY_BODY = 1; @@ -339,7 +341,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { while (!bb.isEmpty()) { ByteBuffer b = bb.remove(); try { - outputStream.write(b.array(), b.arrayOffset() + b.position(), b.remaining()); + ByteBufferList.writeOutputStream(outputStream, b); } finally { copy.add(b); @@ -676,6 +678,11 @@ public CachedSSLSocket(EntryCacheResponse cacheResponse, long contentLength) { super(cacheResponse, contentLength); } + @Override + public SSLEngine getSSLEngine() { + return null; + } + @Override public X509Certificate[] getPeerCertificates() { return null; diff --git a/AndroidAsync/src/com/koushikdutta/async/parser/StringParser.java b/AndroidAsync/src/com/koushikdutta/async/parser/StringParser.java index 19178ccf1..397b62580 100644 --- a/AndroidAsync/src/com/koushikdutta/async/parser/StringParser.java +++ b/AndroidAsync/src/com/koushikdutta/async/parser/StringParser.java @@ -7,17 +7,20 @@ import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.TransformFuture; +import java.nio.charset.Charset; + /** * Created by koush on 5/27/13. */ public class StringParser implements AsyncParser { @Override public Future parse(DataEmitter emitter) { + final String charset = emitter.charset(); return new ByteBufferListParser().parse(emitter) .then(new TransformFuture() { @Override protected void transform(ByteBufferList result) throws Exception { - setComplete(result.readString(null)); + setComplete(result.readString(Charset.forName(charset))); } }); } diff --git a/AndroidAsync/src/com/koushikdutta/async/util/Allocator.java b/AndroidAsync/src/com/koushikdutta/async/util/Allocator.java index a7d4f44e7..608026dc2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/util/Allocator.java +++ b/AndroidAsync/src/com/koushikdutta/async/util/Allocator.java @@ -40,8 +40,9 @@ public int getMinAlloc() { return minAlloc; } - public void setMinAlloc(int minAlloc ) { + public Allocator setMinAlloc(int minAlloc ) { this.minAlloc = minAlloc; + return this; } } diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/ParserTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/ParserTests.java index 468f4c07a..3374eff90 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/ParserTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/ParserTests.java @@ -35,6 +35,11 @@ public boolean isPaused() { public void testUtf8String() throws Exception { StringParser p = new StringParser(); FilteredDataEmitter f = new FilteredDataEmitter() { + @Override + public String charset() { + return Charsets.UTF_8.name(); + } + @Override public boolean isPaused() { return false; From c39cfbe0789d91ccc20fede0f1b31dd87bf58ba2 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 17 Jul 2014 00:58:52 -0700 Subject: [PATCH 026/399] derp. --- AndroidAsync/build.gradle | 2 +- .../koushikdutta/async/ByteBufferList.java | 20 ++++++++++++++++++- .../async/parser/StringParser.java | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/AndroidAsync/build.gradle b/AndroidAsync/build.gradle index 38c3f67e6..81b428fb3 100644 --- a/AndroidAsync/build.gradle +++ b/AndroidAsync/build.gradle @@ -18,7 +18,7 @@ android { main { manifest.srcFile 'AndroidManifest.xml' - jniLibs.srcDirs = ['libs/'] +// jniLibs.srcDirs = ['libs/'] java.srcDirs=['src/' // , 'okhttp/' diff --git a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java index c036bdf84..fe5b50d1c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java +++ b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java @@ -83,7 +83,25 @@ public int remaining() { public boolean hasRemaining() { return remaining() > 0; } - + + public short peekShort() { + return read(2).duplicate().getShort(); + } + + public int peekInt() { + return read(4).duplicate().getInt(); + } + + public long peekLong() { + return read(8).duplicate().getLong(); + } + + public byte[] peekBytes(int size) { + byte[] ret = new byte[size]; + read(size).duplicate().get(ret); + return ret; + } + public int getInt() { int ret = read(4).getInt(); remaining -= 4; diff --git a/AndroidAsync/src/com/koushikdutta/async/parser/StringParser.java b/AndroidAsync/src/com/koushikdutta/async/parser/StringParser.java index 397b62580..89a611c3c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/parser/StringParser.java +++ b/AndroidAsync/src/com/koushikdutta/async/parser/StringParser.java @@ -20,7 +20,7 @@ public Future parse(DataEmitter emitter) { .then(new TransformFuture() { @Override protected void transform(ByteBufferList result) throws Exception { - setComplete(result.readString(Charset.forName(charset))); + setComplete(result.readString(charset != null ? Charset.forName(charset) : null)); } }); } From 8485b8dd2673f46a69a8f4a7d6174cf5d610bb1f Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Fri, 18 Jul 2014 13:25:20 -0700 Subject: [PATCH 027/399] spdy-work --- .gitignore | 1 + AndroidAsync/.classpath | 2 +- AndroidAsync/build.gradle | 9 + AndroidAsync/project.properties | 2 +- .../koushikdutta/async/ByteBufferList.java | 4 +- .../async/http/spdy/AsyncSpdyConnection.java | 402 ++++++++++++++++++ .../async/http/spdy/ByteBufferListSource.java | 174 ++++++++ .../async/http/spdy/SpdyMiddleware.java | 158 +++++++ .../async/test/ConscryptTests.java | 266 ++++++++++++ .../koushikdutta/async/test/OkHttpTest.java | 90 ++++ 10 files changed, 1104 insertions(+), 4 deletions(-) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java create mode 100644 AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java create mode 100644 AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java diff --git a/.gitignore b/.gitignore index b9a860d79..30c8bb1d2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build okhttp okio +libs diff --git a/AndroidAsync/.classpath b/AndroidAsync/.classpath index c06dfcb8e..51769745b 100644 --- a/AndroidAsync/.classpath +++ b/AndroidAsync/.classpath @@ -1,7 +1,7 @@ - + diff --git a/AndroidAsync/build.gradle b/AndroidAsync/build.gradle index 81b428fb3..908138bd5 100644 --- a/AndroidAsync/build.gradle +++ b/AndroidAsync/build.gradle @@ -39,6 +39,15 @@ android { targetCompatibility JavaVersion.VERSION_1_7 } + lintOptions { + abortOnError false + } + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 19 + } + compileSdkVersion 19 buildToolsVersion "20.0.0" } diff --git a/AndroidAsync/project.properties b/AndroidAsync/project.properties index fe357b1dd..edc832b2c 100644 --- a/AndroidAsync/project.properties +++ b/AndroidAsync/project.properties @@ -11,7 +11,7 @@ #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-L +target=android-19 android.library=true diff --git a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java index fe5b50d1c..a27f47100 100644 --- a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java +++ b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java @@ -114,8 +114,8 @@ public char getByteChar() { return ret; } - public int getShort() { - int ret = read(2).getShort(); + public short getShort() { + short ret = read(2).getShort(); remaining -= 2; return ret; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java new file mode 100644 index 000000000..c92f51ce6 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -0,0 +1,402 @@ +package com.koushikdutta.async.http.spdy; + +import android.text.TextUtils; + +import com.koushikdutta.async.AsyncServer; +import com.koushikdutta.async.AsyncSocket; +import com.koushikdutta.async.BufferedDataEmitter; +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.Util; +import com.koushikdutta.async.callback.CompletedCallback; +import com.koushikdutta.async.callback.DataCallback; +import com.koushikdutta.async.callback.WritableCallback; +import com.koushikdutta.async.http.spdy.okhttp.Protocol; +import com.koushikdutta.async.http.spdy.okhttp.internal.NamedRunnable; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.ErrorCode; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameReader; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameWriter; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.HeadersMode; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Http20Draft13; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Ping; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.SpdyConnection; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.SpdyStream; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Variant; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.ByteString; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE; + +/** + * Created by koush on 7/16/14. + */ +public class AsyncSpdyConnection implements FrameReader.Handler { + BufferedDataEmitter emitter; + AsyncSocket socket; + FrameReader reader; + FrameWriter writer; + Variant variant; + ByteBufferListSource source = new ByteBufferListSource(); + Hashtable sockets = new Hashtable(); + Protocol protocol; + boolean client = true; + + private class SpdySocket implements AsyncSocket { + long bytesLeftInWriteWindow; + WritableCallback writable; + final int id; + CompletedCallback closedCallback; + CompletedCallback endCallback; + DataCallback dataCallback; + ByteBufferList pending = new ByteBufferList(); + + public SpdySocket(int id, boolean outFinished, boolean inFinished, List
headerBlock) { + this.id = id; + } + + private void report(Exception e) { + if (endCallback != null) + endCallback.onCompleted(e); + } + + public boolean isLocallyInitiated() { + boolean streamIsClient = ((id & 1) == 1); + return client == streamIsClient; + } + + public void addBytesToWriteWindow(long delta) { + long prev = bytesLeftInWriteWindow; + bytesLeftInWriteWindow += delta; + if (writable != null && bytesLeftInWriteWindow > 0 && prev <= 0) + writable.onWriteable(); + } + + @Override + public AsyncServer getServer() { + return socket.getServer(); + } + + @Override + public void setDataCallback(DataCallback callback) { + dataCallback = callback; + } + + @Override + public DataCallback getDataCallback() { + return dataCallback; + } + + @Override + public boolean isChunked() { + return false; + } + + boolean paused; + @Override + public void pause() { + paused = true; + } + + @Override + public void resume() { + paused = false; + } + + @Override + public void close() { + + } + + @Override + public boolean isPaused() { + return paused; + } + + @Override + public void setEndCallback(CompletedCallback callback) { + endCallback = callback; + } + + @Override + public CompletedCallback getEndCallback() { + return endCallback; + } + + @Override + public String charset() { + return null; + } + + @Override + public void write(ByteBuffer bb) { + + } + + @Override + public void write(ByteBufferList bb) { + + } + + @Override + public void setWriteableCallback(WritableCallback handler) { + writable = handler; + } + + @Override + public WritableCallback getWriteableCallback() { + return writable; + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public void end() { + } + + @Override + public void setClosedCallback(CompletedCallback handler) { + closedCallback = handler; + } + + @Override + public CompletedCallback getClosedCallback() { + return closedCallback; + } + } + + public AsyncSpdyConnection(AsyncSocket socket, Protocol protocol) { + this.protocol = protocol; + this.socket = socket; + emitter = new BufferedDataEmitter(socket); + emitter.setDataCallback(callback); + + if (protocol == Protocol.SPDY_3) { + variant = new Spdy3(); + } + else if (protocol == Protocol.HTTP_2) { + variant = new Http20Draft13(); + } + reader = variant.newReader(source, true); + } + + DataCallback callback = new DataCallback() { + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + bb.get(source); + if (!reader.canProcessFrame(source)) + return; + try { + reader.nextFrame(AsyncSpdyConnection.this); + } + catch (IOException e) { + throw new AssertionError(e); + } + } + }; + + /** Even, positive numbered streams are pushed streams in HTTP/2. */ + private boolean pushedStream(int streamId) { + return protocol == Protocol.HTTP_2 && streamId != 0 && (streamId & 1) == 0; + } + + @Override + public void data(boolean inFinished, int streamId, BufferedSource source, int length) throws IOException { + if (pushedStream(streamId)) { + throw new AssertionError("push"); +// pushDataLater(streamId, source, length, inFinished); +// return; + } + SpdySocket socket = sockets.get(streamId); + if (socket == null) { + writer.rstStream(streamId, ErrorCode.INVALID_STREAM); + source.skip(length); + return; + } + if (source != this.source) + throw new AssertionError(); + this.source.get(socket.pending, length); + Util.emitAllData(socket, socket.pending); + if (inFinished) { + socket.report(null); + } + } + + private int lastGoodStreamId; + private int nextStreamId; + @Override + public void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, List
headerBlock, HeadersMode headersMode) { + if (pushedStream(streamId)) { + throw new AssertionError("push"); +// pushHeadersLater(streamId, headerBlock, inFinished); +// return; + } + + // If we're shutdown, don't bother with this stream. + if (shutdown) return; + + SpdySocket socket = sockets.get(streamId); + + if (socket == null) { + // The headers claim to be for an existing stream, but we don't have one. + if (headersMode.failIfStreamAbsent()) { + try { + writer.rstStream(streamId, ErrorCode.INVALID_STREAM); + return; + } + catch (IOException e) { + throw new AssertionError(e); + } + } + + // 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. + socket = new SpdySocket(streamId, outFinished, inFinished, headerBlock); + lastGoodStreamId = streamId; + sockets.put(streamId, socket); + handler.receive(newStream); + return; + } + + // 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; + } + + // Update an existing stream. + stream.receiveHeaders(headerBlock, headersMode); + if (inFinished) stream.receiveFin(); + } + + @Override + public void rstStream(int streamId, ErrorCode errorCode) { + if (pushedStream(streamId)) { + throw new AssertionError("push"); +// pushResetLater(streamId, errorCode); +// return; + } + SpdySocket rstStream = sockets.remove(streamId); + if (rstStream != null) { + rstStream.report(new IOException(errorCode.toString())); + } + } + + Settings peerSettings = new Settings(); + private boolean receivedInitialPeerSettings = false; + @Override + public void settings(boolean clearPrevious, Settings settings) { + long delta = 0; + int priorWriteWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); + if (clearPrevious) + peerSettings.clear(); + peerSettings.merge(settings); + try { + writer.ackSettings(); + } catch (IOException e) { + throw new AssertionError(e); + } + int peerInitialWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); + if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) { + delta = peerInitialWindowSize - priorWriteWindowSize; + if (!receivedInitialPeerSettings) { + addBytesToWriteWindow(delta); + receivedInitialPeerSettings = true; + } + } + for (SpdySocket socket: sockets.values()) { + socket.addBytesToWriteWindow(delta); + } + } + + @Override + public void ackSettings() { + } + + private Map pings; + private void writePing(boolean reply, int payload1, int payload2, Ping ping) throws IOException { + if (ping != null) ping.send(); + writer.ping(reply, payload1, payload2); + } + + private synchronized Ping removePing(int id) { + return pings != null ? pings.remove(id) : null; + } + + @Override + public void ping(boolean ack, int payload1, int payload2) { + if (ack) { + 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. + try { + writePing(true, payload1, payload2, null); + } + catch (IOException e) { + throw new AssertionError(e); + } + } + } + + boolean shutdown; + @Override + public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { + shutdown = true; + + // Fail all streams created after the last good stream ID. + for (Iterator> i = sockets.entrySet().iterator(); + i.hasNext(); ) { + Map.Entry entry = i.next(); + int streamId = entry.getKey(); + if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) { + entry.getValue().report(new IOException(ErrorCode.REFUSED_STREAM.toString())); + i.remove(); + } + } + } + + @Override + public void windowUpdate(int streamId, long windowSizeIncrement) { + System.out.println("fff"); + + } + + @Override + public void priority(int streamId, int streamDependency, int weight, boolean exclusive) { + System.out.println("fff"); + + } + + @Override + public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) throws IOException { + System.out.println("fff"); + + } + + @Override + public void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge) { + System.out.println("fff"); + + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java new file mode 100644 index 000000000..5d57c772d --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java @@ -0,0 +1,174 @@ +package com.koushikdutta.async.http.spdy; + +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okio.Sink; +import com.koushikdutta.async.http.spdy.okio.Timeout; +import com.koushikdutta.async.util.Charsets; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; +import java.nio.charset.Charset; + +/** + * Created by koush on 7/17/14. + */ +public class ByteBufferListSource extends ByteBufferList implements BufferedSource { + @Override + public Buffer buffer() { + return null; + } + + @Override + public boolean exhausted() throws IOException { + return !hasRemaining(); + } + + @Override + public void require(long byteCount) throws IOException { + if (remaining() < byteCount) + throw new AssertionError("out of data"); + } + + @Override + public byte readByte() throws IOException { + return order(ByteOrder.BIG_ENDIAN).get(); + } + + @Override + public short readShort() throws IOException { + return order(ByteOrder.BIG_ENDIAN).getShort(); + } + + @Override + public short readShortLe() throws IOException { + return order(ByteOrder.LITTLE_ENDIAN).getShort(); + } + + @Override + public int readInt() throws IOException { + return order(ByteOrder.BIG_ENDIAN).getInt(); + } + + @Override + public int readIntLe() throws IOException { + return order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + @Override + public long readLong() throws IOException { + return order(ByteOrder.BIG_ENDIAN).getLong(); + } + + @Override + public long readLongLe() throws IOException { + return order(ByteOrder.LITTLE_ENDIAN).getLong(); + } + + @Override + public void skip(long byteCount) throws IOException { + if (byteCount > Integer.MAX_VALUE) + throw new AssertionError("too much skippy, use less peanut butter"); + read(new byte[(int)byteCount]); + } + + @Override + public ByteString readByteString() throws IOException { + return readByteString(remaining()); + } + + @Override + public ByteString readByteString(long byteCount) throws IOException { + return ByteString.of(readByteArray(byteCount)); + } + + @Override + public byte[] readByteArray() throws IOException { + return getAllByteArray(); + } + + @Override + public byte[] readByteArray(long byteCount) throws IOException { + byte[] ret = new byte[(int)byteCount]; + get(ret); + return ret; + } + + @Override + public int read(byte[] sink) throws IOException { + return read(sink, 0, sink.length); + } + + @Override + public void readFully(byte[] sink) throws IOException { + read(sink, 0, sink.length); + } + + @Override + public int read(byte[] sink, int offset, int byteCount) throws IOException { + get(sink, offset, byteCount); + return byteCount; + } + + @Override + public void readFully(Buffer sink, long byteCount) throws IOException { + throw new AssertionError("not implemented"); + } + + @Override + public long readAll(Sink sink) throws IOException { + throw new AssertionError("not implemented"); + } + + @Override + public String readUtf8() throws IOException { + return readUtf8(remaining()); + } + + @Override + public String readUtf8(long byteCount) throws IOException { + return new String(readByteArray(byteCount), Charsets.UTF_8); + } + + @Override + public String readUtf8Line() throws IOException { + throw new AssertionError("not implemented"); + } + + @Override + public String readUtf8LineStrict() throws IOException { + throw new AssertionError("not implemented"); + } + + @Override + public String readString(long byteCount, Charset charset) throws IOException { + return new String(readByteArray(byteCount), charset); + } + + @Override + public long indexOf(byte b) throws IOException { + throw new AssertionError("not implemented"); + } + + @Override + public InputStream inputStream() { + throw new AssertionError("not implemented"); + } + + @Override + public long read(Buffer sink, long byteCount) throws IOException { + throw new AssertionError("not implemented"); + } + + @Override + public Timeout timeout() { + return null; + } + + @Override + public void close() throws IOException { + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java new file mode 100644 index 000000000..bddf203c0 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -0,0 +1,158 @@ +package com.koushikdutta.async.http.spdy; + +import com.koushikdutta.async.AsyncSSLSocket; +import com.koushikdutta.async.AsyncSSLSocketWrapper; +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.callback.ConnectCallback; +import com.koushikdutta.async.future.Cancellable; +import com.koushikdutta.async.http.AsyncHttpClient; +import com.koushikdutta.async.http.AsyncSSLEngineConfigurator; +import com.koushikdutta.async.http.AsyncSSLSocketMiddleware; +import com.koushikdutta.async.http.spdy.okhttp.Protocol; +import com.koushikdutta.async.util.Charsets; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +public class SpdyMiddleware extends AsyncSSLSocketMiddleware { + public SpdyMiddleware(AsyncHttpClient client) { + super(client); + addEngineConfigurator(new AsyncSSLEngineConfigurator() { + @Override + public void configureEngine(SSLEngine engine, String host, int port) { + configure(engine, host, port); + } + }); + } + + static byte[] concatLengthPrefixed(Protocol... protocols) { + ByteBuffer result = ByteBuffer.allocate(8192); + for (Protocol protocol: protocols) { + if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for NPN. + result.put((byte) protocol.toString().length()); + result.put(protocol.toString().getBytes(Charsets.UTF_8)); + } + result.flip(); + byte[] ret = new ByteBufferList(result).getAllByteArray(); + return ret; + } + + @Override + protected AsyncSSLSocketWrapper.HandshakeCallback createHandshakeCallback(final ConnectCallback callback) { + return new AsyncSSLSocketWrapper.HandshakeCallback() { + @Override + public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { + if (e != null || nativeGetAlpnNegotiatedProtocol == null) { + callback.onConnectCompleted(e, socket); + return; + } + try { + long ptr = (long)sslNativePointer.get(socket.getSSLEngine()); + byte[] proto = (byte[])nativeGetAlpnNegotiatedProtocol.invoke(null, ptr); + String protoString = new String(proto); + AsyncSpdyConnection connection = new AsyncSpdyConnection(socket, Protocol.get(protoString)); + } + catch (Exception ex) { + socket.close(); + callback.onConnectCompleted(ex, null); + } + } + }; + } + + private void configure(SSLEngine engine, String host, int port) { + if (!initialized) { + initialized = true; + try { + peerHost = engine.getClass().getSuperclass().getDeclaredField("peerHost"); + peerPort = engine.getClass().getSuperclass().getDeclaredField("peerPort"); + sslParameters = engine.getClass().getDeclaredField("sslParameters"); + npnProtocols = sslParameters.getType().getDeclaredField("npnProtocols"); + alpnProtocols = sslParameters.getType().getDeclaredField("alpnProtocols"); + useSni = sslParameters.getType().getDeclaredField("useSni"); + sslNativePointer = engine.getClass().getDeclaredField("sslNativePointer"); + String nativeCryptoName = sslParameters.getType().getPackage().getName() + ".NativeCrypto"; + nativeGetNpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) + .getDeclaredMethod("SSL_get_npn_negotiated_protocol", long.class); + nativeGetAlpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) + .getDeclaredMethod("SSL_get0_alpn_selected", long.class); + + peerHost.setAccessible(true); + peerPort.setAccessible(true); + sslParameters.setAccessible(true); + npnProtocols.setAccessible(true); + alpnProtocols.setAccessible(true); + useSni.setAccessible(true); + sslNativePointer.setAccessible(true); + nativeGetNpnNegotiatedProtocol.setAccessible(true); + nativeGetAlpnNegotiatedProtocol.setAccessible(true); + } + catch (Exception e) { + sslParameters = null; + npnProtocols = null; + alpnProtocols = null; + useSni = null; + sslNativePointer = null; + nativeGetNpnNegotiatedProtocol = null; + nativeGetAlpnNegotiatedProtocol = null; + } + } + + if (sslParameters != null) { + try { + byte[] protocols = concatLengthPrefixed( + Protocol.HTTP_1_1, + Protocol.SPDY_3 + ); + + peerHost.set(engine, host); + peerPort.set(engine, port); + Object sslp = sslParameters.get(engine); +// npnProtocols.set(sslp, protocols); + alpnProtocols.set(sslp, protocols); + useSni.set(sslp, true); + } + catch (Exception e ) { + e.printStackTrace(); + } + } + } + + @Override + protected SSLEngine createConfiguredSSLEngine(String host, int port) { + SSLContext sslContext = getSSLContext(); + SSLEngine sslEngine = sslContext.createSSLEngine(); + + for (AsyncSSLEngineConfigurator configurator : engineConfigurators) { + configurator.configureEngine(sslEngine, host, port); + } + + return sslEngine; + } + + boolean initialized; + Field peerHost; + Field peerPort; + Field sslParameters; + Field npnProtocols; + Field alpnProtocols; + Field sslNativePointer; + Field useSni; + Method nativeGetNpnNegotiatedProtocol; + Method nativeGetAlpnNegotiatedProtocol; + + @Override + public void setSSLContext(SSLContext sslContext) { + super.setSSLContext(sslContext); + initialized = false; + } + + @Override + public Cancellable getSocket(GetSocketData data) { + return super.getSocket(data); + } +} \ No newline at end of file diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java new file mode 100644 index 000000000..1322e0390 --- /dev/null +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java @@ -0,0 +1,266 @@ +/* + * Copyright 2013 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.koushikdutta.async.test; + + +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.ErrorCode; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameReader; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.HeadersMode; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okio.Okio; + +import junit.framework.TestCase; + +import org.conscrypt.OpenSSLEngineImpl; +import org.conscrypt.OpenSSLProvider; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.Security; +import java.util.List; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; + +/** + * Created by koush on 7/15/14. + */ +public class ConscryptTests extends TestCase { + boolean initialized; + Field peerHost; + Field peerPort; + Field sslParameters; + Field npnProtocols; + Field alpnProtocols; + Field sslNativePointer; + Field useSni; + Method nativeGetNpnNegotiatedProtocol; + Method nativeGetAlpnNegotiatedProtocol; + + private void configure(SSLEngine engine, String host, int port) throws Exception { + if (!initialized) { + initialized = true; + peerHost = engine.getClass().getSuperclass().getDeclaredField("peerHost"); + peerPort = engine.getClass().getSuperclass().getDeclaredField("peerPort"); + sslParameters = engine.getClass().getDeclaredField("sslParameters"); + npnProtocols = sslParameters.getType().getDeclaredField("npnProtocols"); + alpnProtocols = sslParameters.getType().getDeclaredField("alpnProtocols"); + useSni = sslParameters.getType().getDeclaredField("useSni"); + sslNativePointer = engine.getClass().getDeclaredField("sslNativePointer"); + String nativeCryptoName = sslParameters.getType().getPackage().getName() + ".NativeCrypto"; + nativeGetNpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) + .getDeclaredMethod("SSL_get_npn_negotiated_protocol", long.class); + nativeGetAlpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) + .getDeclaredMethod("SSL_get0_alpn_selected", long.class); + + peerHost.setAccessible(true); + peerPort.setAccessible(true); + sslParameters.setAccessible(true); + npnProtocols.setAccessible(true); + alpnProtocols.setAccessible(true); + useSni.setAccessible(true); + sslNativePointer.setAccessible(true); + nativeGetNpnNegotiatedProtocol.setAccessible(true); + nativeGetAlpnNegotiatedProtocol.setAccessible(true); + } + + byte[] protocols = concatLengthPrefixed( + "http/1.1", + "spdy/3.1" + ); + + peerHost.set(engine, host); + peerPort.set(engine, port); + Object sslp = sslParameters.get(engine); +// npnProtocols.set(sslp, protocols); + alpnProtocols.set(sslp, protocols); + useSni.set(sslp, true); + } + + static byte[] concatLengthPrefixed(String... protocols) { + ByteBuffer result = ByteBuffer.allocate(8192); + for (String protocol: protocols) { + result.put((byte) protocol.toString().length()); + result.put(protocol.toString().getBytes(Charset.forName("UTF-8"))); + } + result.flip(); + byte[] ret = new byte[result.remaining()]; + result.get(ret); + return ret; + } + + public void testConscryptSSLEngineNPNHandshakeBug() throws Exception { + Security.insertProviderAt(new OpenSSLProvider("MyNameBlah"), 1); + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, null, null); + + OpenSSLEngineImpl engine = (OpenSSLEngineImpl)ctx.createSSLEngine(); + configure(engine, "www.google.com", 443); + engine.setUseClientMode(true); + engine.beginHandshake(); + + Socket socket = new Socket(); + socket.connect(new InetSocketAddress("www.google.com", 443)); + + InputStream is = socket.getInputStream(); + OutputStream os = socket.getOutputStream(); + + byte[] buf = new byte[65536]; + ByteBuffer unwrap = null; + ByteBuffer dummy = ByteBuffer.allocate(65536); + + SSLEngineResult.HandshakeStatus handshakeStatus = engine.getHandshakeStatus(); + + while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED + && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { + if (handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_UNWRAP) { + System.out.println("waiting for read... " + engine.getHandshakeStatus()); + int read = is.read(buf); + System.out.println("read: " + read); + if (read <= 0) + throw new Exception("closed!"); + + if (unwrap != null) { + int bufLen = unwrap.remaining() + read; + ByteBuffer b = ByteBuffer.allocate(bufLen); + b.put(unwrap); + b.put(buf, 0, read); + b.flip(); + unwrap = b; + } + else { + unwrap = ByteBuffer.wrap(buf, 0, read); + } + + if (!unwrap.hasRemaining()) { + unwrap = null; + } + + dummy.clear(); + SSLEngineResult res = engine.unwrap(unwrap, dummy); + System.out.println("data remaining after unwrap: " + unwrap.remaining()); + handshakeStatus = res.getHandshakeStatus(); + } + + if (handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_WRAP) { + dummy.clear(); + SSLEngineResult res = engine.wrap(ByteBuffer.allocate(0), dummy); + handshakeStatus = res.getHandshakeStatus(); + dummy.flip(); + if (dummy.hasRemaining()) { + os.write(dummy.array(), 0, dummy.remaining()); + } + } + else if (handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_TASK) { + engine.getDelegatedTask().run(); + } + } + + + System.out.println("Done handshaking! Thank you come again."); + long ptr = (long)sslNativePointer.get(engine); + byte[] proto = (byte[]) nativeGetAlpnNegotiatedProtocol.invoke(null, ptr); +// byte[] proto = (byte[]) nativeGetNpnNegotiatedProtocol.invoke(null, ptr); + String protoString = new String(proto); + System.out.println("negotiated protocol was: " + protoString); + assertEquals(protoString, "spdy/3.1"); + + dummy.clear(); + SSLEngineResult res = engine.unwrap(unwrap, dummy); + dummy.flip(); + byte[] frame = new byte[dummy.remaining()]; + dummy.get(frame ); + Spdy3 spdy3 = new Spdy3(); + BufferedSource source = Okio.buffer(Okio.source(new ByteArrayInputStream(frame))); + FrameReader frameReader = spdy3.newReader(source, true); + ByteBufferList bb = new ByteBufferList(ByteBuffer.wrap(frame)); + assertTrue(frameReader.canProcessFrame(bb)); + + frameReader.nextFrame(new FrameReader.Handler() { + @Override + public void data(boolean inFinished, int streamId, BufferedSource source, int length) throws IOException { + + } + + @Override + public void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, List
headerBlock, HeadersMode headersMode) { + + } + + @Override + public void rstStream(int streamId, ErrorCode errorCode) { + + } + + @Override + public void settings(boolean clearPrevious, Settings settings) { + + } + + @Override + public void ackSettings() { + + } + + @Override + public void ping(boolean ack, int payload1, int payload2) { + + } + + @Override + public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { + + } + + @Override + public void windowUpdate(int streamId, long windowSizeIncrement) { + + } + + @Override + public void priority(int streamId, int streamDependency, int weight, boolean exclusive) { + + } + + @Override + public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) throws IOException { + + } + + @Override + public void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge) { + + } + }); + + + } +} diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java b/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java new file mode 100644 index 000000000..d63494b63 --- /dev/null +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java @@ -0,0 +1,90 @@ +package com.koushikdutta.async.test; + + +import android.test.AndroidTestCase; + +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.http.spdy.okhttp.Handshake; +import com.koushikdutta.async.http.spdy.okhttp.Protocol; +import com.koushikdutta.async.util.Charsets; + +import org.conscrypt.OpenSSLProvider; + +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.security.Security; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; + +public class OkHttpTest extends AndroidTestCase { + public void testOkHttp() throws Exception { +// Context context = getContext().getApplicationContext(); +// Context gms = context.createPackageContext("com.google.android.gms", Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); +// gms +// .getClassLoader() +// .loadClass("com.google.android.gms.common.security.ProviderInstallerImpl") +// .getMethod("insertProvider", Context.class) +// .invoke(null, context); + Security.insertProviderAt(new OpenSSLProvider("MyNameBlah"), 1); + + Class openSslSocketClass; + Method setUseSessionTickets; + Method setHostname; + openSslSocketClass = Class.forName("org.conscrypt.OpenSSLSocketImpl"); + setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class); + setHostname = openSslSocketClass.getMethod("setHostname", String.class); + Method trafficStatsTagSocket = null; + Method trafficStatsUntagSocket = null; + Class trafficStats = Class.forName("android.net.TrafficStats"); + trafficStatsTagSocket = trafficStats.getMethod("tagSocket", Socket.class); + trafficStatsUntagSocket = trafficStats.getMethod("untagSocket", Socket.class); + + // Attempt to find Android 4.1+ APIs. + Method setNpnProtocols = null; + Method getNpnSelectedProtocol = null; + setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class); + getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol"); + + +// Platform p = Platform.get(); + + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, null, null); + Socket socket = SocketFactory.getDefault().createSocket(); + socket.connect(new InetSocketAddress("www.google.com", 443)); + socket = ctx.getSocketFactory().createSocket(socket, "www.google.com", 443, true); + SSLSocket sslSocket = (SSLSocket) socket; + + setUseSessionTickets.invoke(sslSocket, true); + setHostname.invoke(sslSocket, "www.google.com"); + setNpnProtocols.invoke(sslSocket, new Object[] { concatLengthPrefixed(Protocol.HTTP_1_1, Protocol.SPDY_3) }); + + + sslSocket.startHandshake(); + Handshake handshake = Handshake.get(sslSocket.getSession()); + + String proto = new String((byte[])getNpnSelectedProtocol.invoke(sslSocket)); + +// InputStream is = sslSocket.getInputStream(); +// StreamUtility.eat(is); + + System.out.println(proto); + } + + static byte[] concatLengthPrefixed(Protocol... protocols) { + ByteBuffer result = ByteBuffer.allocate(8192); + for (Protocol protocol: protocols) { + if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for NPN. + result.put((byte) protocol.toString().length()); + result.put(protocol.toString().getBytes(Charsets.UTF_8)); + } + result.flip(); + byte[] ret = new ByteBufferList(result).getAllByteArray(); + return ret; + } + +} \ No newline at end of file From 5864287147d01d51bf7948fb05f60bc3ad275775 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Fri, 18 Jul 2014 13:31:25 -0700 Subject: [PATCH 028/399] fixes for eclipse --- .gitignore | 1 + AndroidAsync/AndroidAsync-AndroidAsync.iml | 6 ------ AndroidAsync/build.gradle | 25 +++++++++------------- AndroidAsync/project.properties | 2 +- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index b9a860d79..30c8bb1d2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build okhttp okio +libs diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index f88736b56..1cdbd092c 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -60,12 +60,6 @@ - - - - - - diff --git a/AndroidAsync/build.gradle b/AndroidAsync/build.gradle index 81b428fb3..0a1985d0b 100644 --- a/AndroidAsync/build.gradle +++ b/AndroidAsync/build.gradle @@ -9,24 +9,15 @@ buildscript { apply plugin: 'com.android.library' android { - dependencies { -// compile 'com.squareup.okio:okio:+' -// androidTestCompile 'com.squareup.okhttp:okhttp:1.+' - } - sourceSets { main { manifest.srcFile 'AndroidManifest.xml' -// jniLibs.srcDirs = ['libs/'] + jniLibs.srcDirs = ['libs/'] java.srcDirs=['src/' -// , 'okhttp/' -// , 'okhttp-shim/' -// , '../okio/okio/src/main/java/' - , '../conscrypt/' - , '../compat/' -// , '../okhttp/okhttp/src/main/java/' +// , '../conscrypt/' +// , '../compat/' ] } androidTest.java.srcDirs=['test/src/'] @@ -34,9 +25,13 @@ android { androidTest.assets.srcDirs=['test/assets/'] } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + lintOptions { + abortOnError false + } + + defaultConfig { + minSdkVersion 9 + targetSdkVersion 19 } compileSdkVersion 19 diff --git a/AndroidAsync/project.properties b/AndroidAsync/project.properties index fe357b1dd..edc832b2c 100644 --- a/AndroidAsync/project.properties +++ b/AndroidAsync/project.properties @@ -11,7 +11,7 @@ #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt # Project target. -target=android-L +target=android-19 android.library=true From 499d2772c638e91a6a27b3394efc7e9193b498f6 Mon Sep 17 00:00:00 2001 From: Sean Stuckless Date: Fri, 18 Jul 2014 18:57:31 -0400 Subject: [PATCH 029/399] removed package protected on some fields so that subclasses could access the fields --- .../koushikdutta/async/http/AsyncSSLSocketMiddleware.java | 6 +++--- .../com/koushikdutta/async/http/AsyncSocketMiddleware.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java index b339c935e..82cc3eadb 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java @@ -26,7 +26,7 @@ public AsyncSSLSocketMiddleware(AsyncHttpClient client) { super(client, "https", 443); } - SSLContext sslContext; + protected SSLContext sslContext; public void setSSLContext(SSLContext sslContext) { this.sslContext = sslContext; @@ -36,13 +36,13 @@ public SSLContext getSSLContext() { return sslContext != null ? sslContext : AsyncSSLSocketWrapper.getDefaultSSLContext(); } - TrustManager[] trustManagers; + protected TrustManager[] trustManagers; public void setTrustManagers(TrustManager[] trustManagers) { this.trustManagers = trustManagers; } - HostnameVerifier hostnameVerifier; + protected HostnameVerifier hostnameVerifier; public void setHostnameVerifier(HostnameVerifier hostnameVerifier) { this.hostnameVerifier = hostnameVerifier; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java index 8e8a36e0e..e293d23a0 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java @@ -50,7 +50,7 @@ public AsyncSocketMiddleware(AsyncHttpClient client) { this(client, "http", 80); } - AsyncHttpClient mClient; + protected AsyncHttpClient mClient; protected ConnectCallback wrapCallback(ConnectCallback callback, Uri uri, int port, boolean proxied) { return callback; From 1d43efe595480565fce5389ebe90a6e961509c73 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sat, 19 Jul 2014 00:05:40 -0700 Subject: [PATCH 030/399] remove datasink.write(ByteBuffer). --- AndroidAsync/AndroidAsync-AndroidAsync.iml | 6 ++ AndroidAsync/build.gradle | 18 +---- .../async/AsyncNetworkSocket.java | 32 -------- .../async/AsyncSSLSocketWrapper.java | 76 ++++--------------- .../koushikdutta/async/BufferedDataSink.java | 26 ------- .../src/com/koushikdutta/async/DataSink.java | 4 - .../koushikdutta/async/FilteredDataSink.java | 17 ----- .../src/com/koushikdutta/async/Util.java | 24 +++--- .../src/com/koushikdutta/async/dns/Dns.java | 2 +- .../async/http/AsyncHttpResponse.java | 2 +- .../async/http/AsyncHttpResponseImpl.java | 14 +--- .../async/http/ResponseCacheMiddleware.java | 6 -- .../async/http/WebSocketImpl.java | 17 +---- .../http/filter/ChunkedOutputFilter.java | 9 +++ .../server/AsyncHttpServerResponseImpl.java | 21 ----- .../async/http/spdy/AsyncSpdyConnection.java | 10 +-- .../async/http/spdy/SpdyMiddleware.java | 2 +- .../async/stream/OutputStreamDataSink.java | 17 ----- .../koushikdutta/async/util/Allocator.java | 4 + .../async/test/ConscryptTests.java | 2 +- .../com/koushikdutta/async/test/Issue59.java | 2 +- 21 files changed, 63 insertions(+), 248 deletions(-) diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index 1cdbd092c..f88736b56 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -60,6 +60,12 @@ + + + + + + diff --git a/AndroidAsync/build.gradle b/AndroidAsync/build.gradle index 5f19dba18..e193153d7 100644 --- a/AndroidAsync/build.gradle +++ b/AndroidAsync/build.gradle @@ -16,8 +16,8 @@ android { jniLibs.srcDirs = ['libs/'] java.srcDirs=['src/' -// , '../conscrypt/' -// , '../compat/' + , '../conscrypt/' + , '../compat/' ] } androidTest.java.srcDirs=['test/src/'] @@ -29,20 +29,6 @@ android { abortOnError false } - defaultConfig { - minSdkVersion 9 - targetSdkVersion 19 - } - - lintOptions { - abortOnError false - } - - defaultConfig { - minSdkVersion 9 - targetSdkVersion 19 - } - compileSdkVersion 19 buildToolsVersion "20.0.0" } diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java b/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java index 9a15cb287..3eb5a3cd1 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java @@ -102,38 +102,6 @@ private void handleRemaining(int remaining) { mKey.interestOps(SelectionKey.OP_READ); } } - - @Override - public void write(final ByteBuffer b) { - if (mServer.getAffinity() != Thread.currentThread()) { - mServer.run(new Runnable() { - @Override - public void run() { - write(b); - } - }); - return; - } - try { - if (!mChannel.isConnected()) { - assert !mChannel.isChunked(); - return; - } - - // keep writing until the the socket can't write any more, or the - // data is exhausted. - int before = b.remaining(); - mChannel.write(b); - handleRemaining(b.remaining()); - mServer.onDataSent(before - b.remaining()); - } - catch (IOException ex) { - closeInternal(); - reportEndPending(ex); - reportClose(ex); - } - } - private ByteBufferList pending = new ByteBufferList(); // private ByteBuffer[] buffers = new ByteBuffer[8]; diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 36d087c51..7f609994a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -244,7 +244,7 @@ private void handleHandshakeStatus(HandshakeStatus status) { } if (status == HandshakeStatus.NEED_WRAP) { - write(ByteBufferList.EMPTY_BYTEBUFFER); + write(writeList); } if (status == HandshakeStatus.NEED_UNWRAP) { @@ -315,65 +315,15 @@ private void handleHandshakeStatus(HandshakeStatus status) { } } - private void writeTmp(ByteBuffer mWriteTmp) { - mWriteTmp.flip(); - if (mWriteTmp.remaining() > 0) - mSink.write(mWriteTmp); - assert !mWriteTmp.hasRemaining(); - } - - int calculateAlloc(int remaining) { // alloc 50% more than we need for writing int alloc = remaining * 3 / 2; if (alloc == 0) - alloc = 8182; + alloc = 8192; return alloc; } - @Override - public void write(ByteBuffer bb) { - if (mWrapping) - return; - if (mSink.remaining() > 0) - return; - mWrapping = true; - int remaining; - SSLEngineResult res = null; - ByteBuffer mWriteTmp = ByteBufferList.obtain(calculateAlloc(bb.remaining())); - do { - // if the handshake is finished, don't send - // 0 bytes of data, since that makes the ssl connection die. - // it wraps a 0 byte package, and craps out. - if (finishedHandshake && bb.remaining() == 0) { - mWrapping = false; - return; - } - remaining = bb.remaining(); - try { - res = engine.wrap(bb, mWriteTmp); - writeTmp(mWriteTmp); - int previousCapacity = mWriteTmp.capacity(); - ByteBufferList.reclaim(mWriteTmp); - mWriteTmp = null; - if (res.getStatus() == Status.BUFFER_OVERFLOW) { - mWriteTmp = ByteBufferList.obtain(previousCapacity * 2); - remaining = -1; - } - else { - mWriteTmp = ByteBufferList.obtain(calculateAlloc(bb.remaining())); - } - handleHandshakeStatus(res.getHandshakeStatus()); - } - catch (SSLException e) { - report(e); - } - } - while ((remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)) && mSink.remaining() == 0); - ByteBufferList.reclaim(mWriteTmp); - mWrapping = false; - } - + ByteBufferList writeList = new ByteBufferList(); @Override public void write(ByteBufferList bb) { if (mWrapping) @@ -383,7 +333,7 @@ public void write(ByteBufferList bb) { mWrapping = true; int remaining; SSLEngineResult res = null; - ByteBuffer mWriteTmp = ByteBufferList.obtain(calculateAlloc(bb.remaining())); + ByteBuffer writeBuf = ByteBufferList.obtain(calculateAlloc(bb.remaining())); do { // if the handshake is finished, don't send // 0 bytes of data, since that makes the ssl connection die. @@ -395,18 +345,21 @@ public void write(ByteBufferList bb) { remaining = bb.remaining(); try { ByteBuffer[] arr = bb.getAllArray(); - res = engine.wrap(arr, mWriteTmp); + res = engine.wrap(arr, writeBuf); bb.addAll(arr); - writeTmp(mWriteTmp); - int previousCapacity = mWriteTmp.capacity(); - ByteBufferList.reclaim(mWriteTmp); - mWriteTmp = null; + writeBuf.flip(); + writeList.add(writeBuf); + assert !writeList.hasRemaining(); + if (writeList.remaining() > 0) + mSink.write(writeList); + int previousCapacity = writeBuf.capacity(); + writeBuf = null; if (res.getStatus() == Status.BUFFER_OVERFLOW) { - mWriteTmp = ByteBufferList.obtain(previousCapacity * 2); + writeBuf = ByteBufferList.obtain(previousCapacity * 2); remaining = -1; } else { - mWriteTmp = ByteBufferList.obtain(calculateAlloc(bb.remaining())); + writeBuf = ByteBufferList.obtain(calculateAlloc(bb.remaining())); handleHandshakeStatus(res.getHandshakeStatus()); } } @@ -415,7 +368,6 @@ public void write(ByteBufferList bb) { } } while ((remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)) && mSink.remaining() == 0); - ByteBufferList.reclaim(mWriteTmp); mWrapping = false; } diff --git a/AndroidAsync/src/com/koushikdutta/async/BufferedDataSink.java b/AndroidAsync/src/com/koushikdutta/async/BufferedDataSink.java index ed4f87f11..8daa83637 100644 --- a/AndroidAsync/src/com/koushikdutta/async/BufferedDataSink.java +++ b/AndroidAsync/src/com/koushikdutta/async/BufferedDataSink.java @@ -44,27 +44,6 @@ private void writePending() { ByteBufferList mPendingWrites = new ByteBufferList(); - @Override - public void write(ByteBuffer bb) { - if (remaining() >= getMaxBuffer()) - return; - - boolean needsWrite = true; - if (!mPendingWrites.hasRemaining()) { - needsWrite = false; - mDataSink.write(bb); - } - - if (bb.hasRemaining()) { - ByteBuffer dup = ByteBufferList.obtain(bb.remaining()); - dup.put(bb); - dup.flip(); - mPendingWrites.add(dup); - if (needsWrite) - mDataSink.write(mPendingWrites); - } - } - @Override public void write(ByteBufferList bb) { write(bb, false); @@ -114,11 +93,6 @@ public boolean isOpen() { return mDataSink.isOpen(); } - @Override - public void close() { - mDataSink.close(); - } - boolean endPending; @Override public void end() { diff --git a/AndroidAsync/src/com/koushikdutta/async/DataSink.java b/AndroidAsync/src/com/koushikdutta/async/DataSink.java index 1866be18a..7c6905cb3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/DataSink.java +++ b/AndroidAsync/src/com/koushikdutta/async/DataSink.java @@ -1,18 +1,14 @@ package com.koushikdutta.async; -import java.nio.ByteBuffer; - import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; public interface DataSink { - public void write(ByteBuffer bb); public void write(ByteBufferList bb); public void setWriteableCallback(WritableCallback handler); public WritableCallback getWriteableCallback(); public boolean isOpen(); - public void close(); public void end(); public void setClosedCallback(CompletedCallback handler); public CompletedCallback getClosedCallback(); diff --git a/AndroidAsync/src/com/koushikdutta/async/FilteredDataSink.java b/AndroidAsync/src/com/koushikdutta/async/FilteredDataSink.java index 448eb3b93..8aa1806c2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/FilteredDataSink.java +++ b/AndroidAsync/src/com/koushikdutta/async/FilteredDataSink.java @@ -1,7 +1,5 @@ package com.koushikdutta.async; -import java.nio.ByteBuffer; - public class FilteredDataSink extends BufferedDataSink { public FilteredDataSink(DataSink sink) { super(sink); @@ -12,21 +10,6 @@ public ByteBufferList filter(ByteBufferList bb) { return bb; } - @Override - public final void write(ByteBuffer bb) { - // don't filter and write if currently buffering, unless we know - // that the buffer can fit the entirety of the filtered result - if (isBuffering() && getMaxBuffer() != Integer.MAX_VALUE) - return; - ByteBufferList list = new ByteBufferList(); - byte[] bytes = new byte[bb.remaining()]; - bb.get(bytes); - assert bb.remaining() == 0; - list.add(ByteBuffer.wrap(bytes)); - ByteBufferList filtered = filter(list); - super.write(filtered, true); - } - @Override public final void write(ByteBufferList bb) { // don't filter and write if currently buffering, unless we know diff --git a/AndroidAsync/src/com/koushikdutta/async/Util.java b/AndroidAsync/src/com/koushikdutta/async/Util.java index c065b5201..18c3736cc 100644 --- a/AndroidAsync/src/com/koushikdutta/async/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/Util.java @@ -3,6 +3,7 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; +import com.koushikdutta.async.util.Allocator; import com.koushikdutta.async.wrapper.AsyncSocketWrapper; import com.koushikdutta.async.wrapper.DataEmitterWrapper; @@ -67,7 +68,7 @@ public void onCompleted(Exception ex) { private void cleanup() { ds.setClosedCallback(null); ds.setWriteableCallback(null); - ByteBufferList.reclaim(pending); + pending.recycle(); pending = null; try { is.close(); @@ -76,29 +77,28 @@ private void cleanup() { e.printStackTrace(); } } - ByteBuffer pending; - int mToAlloc = 0; - int maxAlloc = 256 * 1024; + ByteBufferList pending = new ByteBufferList(); + Allocator allocator = new Allocator(); @Override public void onWriteable() { try { do { - if (pending == null || pending.remaining() == 0) { - ByteBufferList.reclaim(pending); - pending = ByteBufferList.obtain(Math.min(Math.max(mToAlloc, 2 << 11), maxAlloc)); + if (!pending.hasRemaining()) { + ByteBuffer b = allocator.allocate(); - long toRead = Math.min(max - totalRead, pending.capacity()); - int read = is.read(pending.array(), 0, (int)toRead); + long toRead = Math.min(max - totalRead, b.capacity()); + int read = is.read(b.array(), 0, (int)toRead); if (read == -1 || totalRead == max) { cleanup(); wrapper.onCompleted(null); return; } - mToAlloc = read * 2; + allocator.track(read); totalRead += read; - pending.position(0); - pending.limit(read); + b.position(0); + b.limit(read); + pending.add(b); } ds.write(pending); diff --git a/AndroidAsync/src/com/koushikdutta/async/dns/Dns.java b/AndroidAsync/src/com/koushikdutta/async/dns/Dns.java index bd1e11b48..bc276d668 100644 --- a/AndroidAsync/src/com/koushikdutta/async/dns/Dns.java +++ b/AndroidAsync/src/com/koushikdutta/async/dns/Dns.java @@ -143,7 +143,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { } }); if (!multicast) - dgram.write(packet); + dgram.write(new ByteBufferList(packet)); else dgram.send(new InetSocketAddress("224.0.0.251", 5353), packet); return ret; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java index 109769a94..fcf0dd8a3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java @@ -6,7 +6,7 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.libcore.ResponseHeaders; -public interface AsyncHttpResponse extends AsyncSocket { +public interface AsyncHttpResponse extends DataEmitter { public void setEndCallback(CompletedCallback handler); public CompletedCallback getEndCallback(); public ResponseHeaders getHeaders(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index 2b77b568e..1bbf46cb6 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -20,7 +20,7 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; -abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements AsyncHttpResponse { +abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements AsyncSocket, AsyncHttpResponse { private AsyncHttpRequestBody mWriter; public AsyncSocket getSocket() { @@ -183,13 +183,6 @@ private void assertContent() { } DataSink mSink; - - @Override - public void write(ByteBuffer bb) { - assertContent(); - mSink.write(bb); - } - @Override public void write(ByteBufferList bb) { assertContent(); @@ -198,11 +191,10 @@ public void write(ByteBufferList bb) { @Override public void end() { - - write(ByteBuffer.wrap(new byte[0])); + if (mSink instanceof ChunkedOutputFilter) + mSink.end(); } - @Override public void setWriteableCallback(WritableCallback handler) { mSink.setWriteableCallback(handler); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java index 851d77dd8..b20099bf0 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java @@ -712,12 +712,6 @@ protected void report(Exception e) { closedCallback.onCompleted(e); } - @Override - public void write(ByteBuffer bb) { - // it's gonna write headers and stuff... whatever - bb.limit(bb.position()); - } - @Override public void write(ByteBufferList bb) { // it's gonna write headers and stuff... whatever diff --git a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java index 8fabcb28b..eda555615 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java @@ -96,7 +96,7 @@ protected void onDisconnect(int code, String reason) { } @Override protected void sendFrame(byte[] frame) { - mSink.write(ByteBuffer.wrap(frame)); + mSink.write(new ByteBufferList(frame)); } }; mParser.setMasking(masking); @@ -215,17 +215,17 @@ public CompletedCallback getEndCallback() { @Override public void send(byte[] bytes) { - mSink.write(ByteBuffer.wrap(mParser.frame(bytes))); + mSink.write(new ByteBufferList((mParser.frame(bytes)))); } @Override public void send(byte[] bytes, int offset, int len) { - mSink.write(ByteBuffer.wrap(mParser.frame(bytes, offset, len))); + mSink.write(new ByteBufferList(mParser.frame(bytes, offset, len))); } @Override public void send(String string) { - mSink.write(ByteBuffer.wrap(mParser.frame(string))); + mSink.write(new ByteBufferList((mParser.frame(string)))); } private StringCallback mStringCallback; @@ -260,15 +260,6 @@ public boolean isBuffering() { return mSink.remaining() > 0; } - @Override - public void write(ByteBuffer bb) { - byte[] buf = new byte[bb.remaining()]; - bb.get(buf); - bb.position(0); - bb.limit(0); - send(buf); - } - @Override public void write(ByteBufferList bb) { byte[] buf = bb.getAllByteArray(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedOutputFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedOutputFilter.java index bec28c09b..f0f18d3b3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedOutputFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedOutputFilter.java @@ -18,4 +18,13 @@ public ByteBufferList filter(ByteBufferList bb) { bb.add(ByteBuffer.wrap("\r\n".getBytes())); return bb; } + + @Override + public void end() { + setMaxBuffer(Integer.MAX_VALUE); + ByteBufferList fin = new ByteBufferList(); + write(fin); + setMaxBuffer(0); + super.end(); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java index bd7211bdb..d1cb9527f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java @@ -48,13 +48,6 @@ public AsyncSocket getSocket() { if (HttpUtil.isKeepAlive(req.getHeaders().getHeaders())) mRawHeaders.set("Connection", "Keep-Alive"); } - - @Override - public void write(ByteBuffer bb) { - if (bb.remaining() == 0) - return; - writeInternal(bb); - } @Override public void write(ByteBufferList bb) { @@ -63,15 +56,6 @@ public void write(ByteBufferList bb) { writeInternal(bb); } - private void writeInternal(ByteBuffer bb) { - assert !mEnded; - if (!mHasWritten) { - initFirstWrite(); - return; - } - mSink.write(bb); - } - private void writeInternal(ByteBufferList bb) { assert !mEnded; if (!mHasWritten) { @@ -316,11 +300,6 @@ public boolean isOpen() { return mSocket.isOpen(); } - @Override - public void close() { - mSocket.close(); - } - @Override public void setClosedCallback(CompletedCallback handler) { mSink.setClosedCallback(handler); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index c92f51ce6..484b6d4e7 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -46,6 +46,7 @@ public class AsyncSpdyConnection implements FrameReader.Handler { FrameReader reader; FrameWriter writer; Variant variant; + SpdySocket zero = new SpdySocket(0, false, false, null); ByteBufferListSource source = new ByteBufferListSource(); Hashtable sockets = new Hashtable(); Protocol protocol; @@ -137,11 +138,6 @@ public String charset() { return null; } - @Override - public void write(ByteBuffer bb) { - - } - @Override public void write(ByteBufferList bb) { @@ -238,6 +234,7 @@ public void data(boolean inFinished, int streamId, BufferedSource source, int le private int nextStreamId; @Override public void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, List
headerBlock, HeadersMode headersMode) { + /* if (pushedStream(streamId)) { throw new AssertionError("push"); // pushHeadersLater(streamId, headerBlock, inFinished); @@ -285,6 +282,7 @@ public void headers(boolean outFinished, boolean inFinished, int streamId, int a // Update an existing stream. stream.receiveHeaders(headerBlock, headersMode); if (inFinished) stream.receiveFin(); + */ } @Override @@ -318,7 +316,7 @@ public void settings(boolean clearPrevious, Settings settings) { if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) { delta = peerInitialWindowSize - priorWriteWindowSize; if (!receivedInitialPeerSettings) { - addBytesToWriteWindow(delta); + zero.addBytesToWriteWindow(delta); receivedInitialPeerSettings = true; } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index bddf203c0..837a12666 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -51,7 +51,7 @@ public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { return; } try { - long ptr = (long)sslNativePointer.get(socket.getSSLEngine()); + long ptr = (Long)sslNativePointer.get(socket.getSSLEngine()); byte[] proto = (byte[])nativeGetAlpnNegotiatedProtocol.invoke(null, ptr); String protoString = new String(proto); AsyncSpdyConnection connection = new AsyncSpdyConnection(socket, Protocol.get(protoString)); diff --git a/AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataSink.java b/AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataSink.java index 13bb419c6..185a8b77e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataSink.java +++ b/AndroidAsync/src/com/koushikdutta/async/stream/OutputStreamDataSink.java @@ -27,11 +27,6 @@ public void end() { } } - @Override - public void close() { - end(); - } - AsyncServer server; public OutputStreamDataSink(AsyncServer server, OutputStream stream) { this.server = server; @@ -47,18 +42,6 @@ public OutputStream getOutputStream() throws IOException { return mStream; } - @Override - public void write(final ByteBuffer bb) { - try { - getOutputStream().write(bb.array(), bb.arrayOffset() + bb.position(), bb.remaining()); - } - catch (IOException e) { - reportClose(e); - } - bb.position(0); - bb.limit(0); - } - @Override public void write(final ByteBufferList bb) { try { diff --git a/AndroidAsync/src/com/koushikdutta/async/util/Allocator.java b/AndroidAsync/src/com/koushikdutta/async/util/Allocator.java index 608026dc2..c25c1a1d7 100644 --- a/AndroidAsync/src/com/koushikdutta/async/util/Allocator.java +++ b/AndroidAsync/src/com/koushikdutta/async/util/Allocator.java @@ -21,6 +21,10 @@ public Allocator() { } public ByteBuffer allocate() { + return allocate(currentAlloc); + } + + public ByteBuffer allocate(int currentAlloc) { return ByteBufferList.obtain(Math.min(Math.max(currentAlloc, minAlloc), maxAlloc)); } diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java index 1322e0390..a50d910f7 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java @@ -186,7 +186,7 @@ else if (handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_TASK) { System.out.println("Done handshaking! Thank you come again."); - long ptr = (long)sslNativePointer.get(engine); + long ptr = (Long)sslNativePointer.get(engine); byte[] proto = (byte[]) nativeGetAlpnNegotiatedProtocol.invoke(null, ptr); // byte[] proto = (byte[]) nativeGetNpnNegotiatedProtocol.invoke(null, ptr); String protoString = new String(proto); diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java b/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java index 44b44fc77..ebaae9ddd 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java @@ -33,7 +33,7 @@ public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerRespo Util.writeAll(response, "foobarbeepboop".getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { - response.close(); + response.end(); } }); } From 6a34fcd2137274579e9a1206839727a3b757bdde Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 21 Jul 2014 14:31:46 -0700 Subject: [PATCH 031/399] refactor our libcore cruft. as much as possible at least. --- .../async/http/AsyncHttpClient.java | 21 +++++----- .../async/http/AsyncHttpClientMiddleware.java | 19 +++++---- .../async/http/AsyncHttpRequest.java | 38 +++++++++--------- .../async/http/AsyncHttpResponse.java | 9 +++-- .../async/http/AsyncHttpResponseImpl.java | 35 +++++++++++----- .../async/http/AsyncSocketMiddleware.java | 6 +-- .../com/koushikdutta/async/http/HttpUtil.java | 40 ++++++++++++++++++- .../async/http/ResponseCacheMiddleware.java | 29 ++++++++------ .../async/http/SimpleMiddleware.java | 8 ++-- .../async/http/WebSocketImpl.java | 12 +++--- .../async/http/callback/HeadersCallback.java | 10 +++++ .../async/http/libcore/HeaderParser.java | 2 + .../async/http/libcore/HttpDate.java | 2 + .../async/http/spdy/HttpTransport.java | 15 +++++++ .../async/test/HttpClientTests.java | 2 +- .../com/koushikdutta/async/test/Issue59.java | 4 +- 16 files changed, 171 insertions(+), 81 deletions(-) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/HttpTransport.java diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index 504d5c602..7f66deb0b 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -191,9 +191,9 @@ private static long getTimeoutRemaining(AsyncHttpRequest request) { } private static void copyHeader(AsyncHttpRequest from, AsyncHttpRequest to, String header) { - String value = from.getHeaders().getHeaders().get(header); + String value = from.getHeaders().get(header); if (!TextUtils.isEmpty(value)) - to.getHeaders().getHeaders().set(header, value); + to.getHeaders().set(header, value); } private void executeAffinity(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) { @@ -283,6 +283,7 @@ protected void onRequestCompleted(Exception ex) { @Override public void setDataEmitter(DataEmitter emitter) { + data.response = this; data.bodyEmitter = emitter; synchronized (mMiddleware) { for (AsyncHttpClientMiddleware middleware: mMiddleware) { @@ -293,8 +294,8 @@ public void setDataEmitter(DataEmitter emitter) { super.setDataEmitter(data.bodyEmitter); - RawHeaders headers = mHeaders.getHeaders(); - int responseCode = headers.getResponseCode(); + RawHeaders headers = mHeaders; + int responseCode = code(); if ((responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == 307) && request.getFollowRedirect()) { String location = headers.get("Location"); Uri redirect; @@ -326,7 +327,7 @@ public void setDataEmitter(DataEmitter emitter) { return; } - request.logv("Final (post cache response) headers:\n" + mHeaders.getHeaders().toHeaderString()); + request.logv("Final (post cache response) headers:\n" + mHeaders.toHeaderString()); // at this point the headers are done being modified reportConnectedCompleted(cancel, null, this, request, callback); @@ -342,7 +343,7 @@ protected void onHeadersReceived() { mServer.removeAllCallbacks(cancel.scheduled); // allow the middleware to massage the headers before the body is decoded - request.logv("Received headers:\n" + mHeaders.getHeaders().toHeaderString()); + request.logv("Received headers:\n" + mHeaders.toHeaderString()); data.headers = mHeaders; synchronized (mMiddleware) { @@ -378,7 +379,7 @@ protected void report(Exception ex) { return; super.report(ex); if (!socket.isOpen() || ex != null) { - if (getHeaders() == null && ex != null) + if (headers() == null && ex != null) reportConnectedCompleted(cancel, ex, null, request, callback); } @@ -546,7 +547,7 @@ public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) { } invokeConnect(callback, response); - final long contentLength = response.getHeaders().getContentLength(); + final long contentLength = HttpUtil.contentLength(response.headers()); response.setDataCallback(new OutputStreamDataCallback(fout) { @Override @@ -591,8 +592,6 @@ public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) { } invokeConnect(callback, response); - final long contentLength = response.getHeaders().getContentLength(); - Future parsed = parser.parse(response) .setCallback(new FutureCallback() { @Override @@ -626,7 +625,7 @@ public void onConnectCompleted(Exception ex, AsyncHttpResponse response) { } return; } - WebSocket ws = WebSocketImpl.finishHandshake(req.getHeaders().getHeaders(), response); + WebSocket ws = WebSocketImpl.finishHandshake(req.getHeaders(), response); if (ws == null) { if (!ret.setComplete(new WebSocketHandshakeException("Unable to complete websocket handshake"))) return; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java index 135ec6947..79deb4c7b 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java @@ -2,13 +2,12 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.future.Cancellable; -import com.koushikdutta.async.http.libcore.ResponseHeaders; +import com.koushikdutta.async.http.libcore.RawHeaders; import com.koushikdutta.async.util.UntypedHashtable; -import java.util.Hashtable; - public interface AsyncHttpClientMiddleware { public static class GetSocketData { public UntypedHashtable state = new UntypedHashtable(); @@ -20,12 +19,17 @@ public static class GetSocketData { public static class OnSocketData extends GetSocketData { public AsyncSocket socket; } - - public static class OnHeadersReceivedData extends OnSocketData { - public ResponseHeaders headers; + + public static class SendHeaderData extends OnSocketData { + CompletedCallback sendHeadersCallback; } - + + public static class OnHeadersReceivedData extends SendHeaderData { + public RawHeaders headers; + } + public static class OnBodyData extends OnHeadersReceivedData { + public AsyncHttpResponse response; public DataEmitter bodyEmitter; } @@ -35,6 +39,7 @@ public static class OnRequestCompleteData extends OnBodyData { public Cancellable getSocket(GetSocketData data); public void onSocket(OnSocketData data); + public boolean sendHeaders(SendHeaderData data); public void onHeadersReceived(OnHeadersReceivedData data); public void onBodyDecoder(OnBodyData data); public void onRequestComplete(OnRequestCompleteData data); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java index 22316b57f..b3eb45b52 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java @@ -114,25 +114,25 @@ public static void setDefaultHeaders(RawHeaders ret, Uri uri) { public AsyncHttpRequest(Uri uri, String method, RawHeaders headers) { assert uri != null; mMethod = method; + this.uri = uri; if (headers == null) mRawHeaders = new RawHeaders(); else mRawHeaders = headers; if (headers == null) setDefaultHeaders(mRawHeaders, uri); - mHeaders = new RequestHeaders(uri, mRawHeaders); mRawHeaders.setStatusLine(getRequestLine().toString()); } + Uri uri; public Uri getUri() { - return mHeaders.getUri(); + return uri; } private RawHeaders mRawHeaders = new RawHeaders(); - private RequestHeaders mHeaders; - public RequestHeaders getHeaders() { - return mHeaders; + public RawHeaders getHeaders() { + return mRawHeaders; } public String getRequestString() { @@ -174,7 +174,7 @@ public AsyncHttpRequest setTimeout(int timeout) { public static AsyncHttpRequest create(HttpRequest request) { AsyncHttpRequest ret = new AsyncHttpRequest(Uri.parse(request.getRequestLine().getUri()), request.getRequestLine().getMethod()); for (Header header: request.getAllHeaders()) { - ret.getHeaders().getHeaders().add(header.getName(), header.getValue()); + ret.getHeaders().add(header.getName(), header.getValue()); } return ret; } @@ -194,25 +194,25 @@ public HttpRequestWrapper(AsyncHttpRequest request) { @Override public void addHeader(Header header) { - request.getHeaders().getHeaders().add(header.getName(), header.getValue()); + request.getHeaders().add(header.getName(), header.getValue()); } @Override public void addHeader(String name, String value) { - request.getHeaders().getHeaders().add(name, value); + request.getHeaders().add(name, value); } @Override public boolean containsHeader(String name) { - return request.getHeaders().getHeaders().get(name) != null; + return request.getHeaders().get(name) != null; } @Override public Header[] getAllHeaders() { - Header[] ret = new Header[request.getHeaders().getHeaders().length()]; + Header[] ret = new Header[request.getHeaders().length()]; for (int i = 0; i < ret.length; i++) { - String name = request.getHeaders().getHeaders().getFieldName(i); - String value = request.getHeaders().getHeaders().getValue(i); + String name = request.getHeaders().getFieldName(i); + String value = request.getHeaders().getValue(i); ret[i] = new BasicHeader(name, value); } return ret; @@ -220,7 +220,7 @@ public Header[] getAllHeaders() { @Override public Header getFirstHeader(String name) { - String value = request.getHeaders().getHeaders().get(name); + String value = request.getHeaders().get(name); if (value == null) return null; return new BasicHeader(name, value); @@ -228,7 +228,7 @@ public Header getFirstHeader(String name) { @Override public Header[] getHeaders(String name) { - Map> map = request.getHeaders().getHeaders().toMultimap(); + Map> map = request.getHeaders().toMultimap(); List vals = map.get(name); if (vals == null) return new Header[0]; @@ -271,12 +271,12 @@ public HeaderIterator headerIterator(String name) { @Override public void removeHeader(Header header) { - request.getHeaders().getHeaders().removeAll(header.getName()); + request.getHeaders().removeAll(header.getName()); } @Override public void removeHeaders(String name) { - request.getHeaders().getHeaders().removeAll(name); + request.getHeaders().removeAll(name); } @Override @@ -286,7 +286,7 @@ public void setHeader(Header header) { @Override public void setHeader(String name, String value) { - request.getHeaders().getHeaders().set(name, value); + request.getHeaders().set(name, value); } @Override @@ -306,12 +306,12 @@ public HttpRequest asHttpRequest() { } public AsyncHttpRequest setHeader(String name, String value) { - getHeaders().getHeaders().set(name, value); + getHeaders().set(name, value); return this; } public AsyncHttpRequest addHeader(String name, String value) { - getHeaders().getHeaders().add(name, value); + getHeaders().add(name, value); return this; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java index fcf0dd8a3..631879c99 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java @@ -2,14 +2,15 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; -import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; -import com.koushikdutta.async.http.libcore.ResponseHeaders; +import com.koushikdutta.async.http.libcore.RawHeaders; public interface AsyncHttpResponse extends DataEmitter { public void setEndCallback(CompletedCallback handler); - public CompletedCallback getEndCallback(); - public ResponseHeaders getHeaders(); + public String protocol(); + public String message(); + public int code(); + public RawHeaders headers(); public void end(); public AsyncSocket detachSocket(); public AsyncHttpRequest getRequest(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index 1bbf46cb6..a3e4189c6 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -40,14 +40,14 @@ void setSocket(AsyncSocket exchange) { mWriter = mRequest.getBody(); if (mWriter != null) { - if (mRequest.getHeaders().getContentType() == null) - mRequest.getHeaders().setContentType(mWriter.getContentType()); + if (mRequest.getHeaders().get("Content-Type") == null) + mRequest.getHeaders().set("Content-Type", mWriter.getContentType()); if (mWriter.length() >= 0) { - mRequest.getHeaders().setContentLength(mWriter.length()); + mRequest.getHeaders().set("Content-Length", String.valueOf(mWriter.length())); mSink = mSocket; } else { - mRequest.getHeaders().getHeaders().set("Transfer-Encoding", "Chunked"); + mRequest.getHeaders().set("Transfer-Encoding", "Chunked"); mSink = new ChunkedOutputFilter(mSocket); } } @@ -116,7 +116,7 @@ else if (!"\r".equals(s)) { mRawHeaders.addLine(s); } else { - mHeaders = new ResponseHeaders(mRequest.getUri(), mRawHeaders); + mHeaders = mRawHeaders; onHeadersReceived(); // socket may get detached after headers (websocket) if (mSocket == null) @@ -161,7 +161,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { private AsyncHttpRequest mRequest; private AsyncSocket mSocket; - ResponseHeaders mHeaders; + protected RawHeaders mHeaders; public AsyncHttpResponseImpl(AsyncHttpRequest request) { mRequest = request; } @@ -169,17 +169,32 @@ public AsyncHttpResponseImpl(AsyncHttpRequest request) { boolean mCompleted = false; @Override - public ResponseHeaders getHeaders() { + public RawHeaders headers() { return mHeaders; } + @Override + public int code() { + return headers().getResponseCode(); + } + + @Override + public String protocol() { + return "HTTP/1." + headers().getHttpMinorVersion(); + } + + @Override + public String message() { + return headers().getResponseMessage(); + } + private boolean mFirstWrite = true; private void assertContent() { if (!mFirstWrite) return; mFirstWrite = false; - assert null != mRequest.getHeaders().getHeaders().get("Content-Type"); - assert mRequest.getHeaders().getHeaders().get("Transfer-Encoding") != null || mRequest.getHeaders().getContentLength() != -1; + assert null != mRequest.getHeaders().get("Content-Type"); + assert mRequest.getHeaders().get("Transfer-Encoding") != null || HttpUtil.contentLength(mRequest.getHeaders()) != -1; } DataSink mSink; @@ -228,7 +243,7 @@ public AsyncServer getServer() { @Override public String charset() { - Multimap mm = Multimap.parseHeader(getHeaders().getHeaders(), "Content-Type"); + Multimap mm = Multimap.parseHeader(headers(), "Content-Type"); String cs; if (mm != null && null != (cs = mm.getString("charset")) && Charset.isSupported(cs)) { return cs; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java index 8e8a36e0e..a50ff9afe 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java @@ -171,14 +171,14 @@ public Cancellable getSocket(final GetSocketData data) { unresolvedHost = data.request.getProxyHost(); unresolvedPort = data.request.getProxyPort(); // set the host and port explicitly for proxied connections - data.request.getHeaders().getHeaders().setStatusLine(data.request.getProxyRequestLine().toString()); + data.request.getHeaders().setStatusLine(data.request.getProxyRequestLine().toString()); proxied = true; } else if (proxyHost != null) { unresolvedHost = proxyHost; unresolvedPort = proxyPort; // set the host and port explicitly for proxied connections - data.request.getHeaders().getHeaders().setStatusLine(data.request.getProxyRequestLine().toString()); + data.request.getHeaders().setStatusLine(data.request.getProxyRequestLine().toString()); proxied = true; } else { @@ -360,7 +360,7 @@ public void onRequestComplete(final OnRequestCompleteData data) { data.socket.close(); return; } - if (!HttpUtil.isKeepAlive(data.headers.getHeaders()) || !HttpUtil.isKeepAlive(data.request.getHeaders().getHeaders())) { + if (!HttpUtil.isKeepAlive(data.headers) || !HttpUtil.isKeepAlive(data.request.getHeaders())) { data.request.logv("closing out socket (not keep alive)"); data.socket.close(); return; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java b/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java index 9b0824127..c30e42b0d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java @@ -14,7 +14,13 @@ import com.koushikdutta.async.http.filter.GZIPInputFilter; import com.koushikdutta.async.http.filter.InflaterInputFilter; import com.koushikdutta.async.http.libcore.RawHeaders; -import com.koushikdutta.async.http.server.UnknownRequestBody; +import com.koushikdutta.async.http.libcore.RequestHeaders; +import com.koushikdutta.async.http.libcore.ResponseHeaders; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; public class HttpUtil { public static AsyncHttpRequestBody getBody(DataEmitter emitter, CompletedCallback reporter, RawHeaders headers) { @@ -130,4 +136,36 @@ public static boolean isKeepAlive(RawHeaders headers) { return keepAlive; } + + public static int contentLength(RawHeaders headers) { + String cl = headers.get("Content-Length"); + if (cl == null) + return -1; + try { + return Integer.parseInt(cl); + } + catch (NumberFormatException e) { + return -1; + } + } + + public static Set varyFields(RawHeaders headers) { + HashSet ret = new HashSet(); + String value = headers.get("Vary"); + if (value == null) + return ret; + for (String varyField : value.split(",")) { + ret.add(varyField.trim()); + } + return ret; + } + + public static boolean isCacheable(RawHeaders requestHeaders, RawHeaders responseHeaders) { + ResponseHeaders r = new ResponseHeaders(null, responseHeaders); + return r.isCacheable(new RequestHeaders(null, requestHeaders)); + } + + public static boolean isNoCache(RawHeaders headers) { + return new RequestHeaders(null, headers).isNoCache(); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java index b20099bf0..a81167905 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java @@ -14,6 +14,7 @@ import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.SimpleCancellable; +import com.koushikdutta.async.http.libcore.RequestHeaders; import com.koushikdutta.async.util.Charsets; import com.koushikdutta.async.http.libcore.RawHeaders; import com.koushikdutta.async.http.libcore.ResponseHeaders; @@ -94,7 +95,7 @@ public void setCaching(boolean caching) { // also see if this can be turned into a conditional cache request. @Override public Cancellable getSocket(final GetSocketData data) { - if (cache == null || !caching || data.request.getHeaders().isNoCache()) { + if (cache == null || !caching || HttpUtil.isNoCache(data.request.getHeaders())) { networkCount++; return null; } @@ -120,7 +121,7 @@ public Cancellable getSocket(final GetSocketData data) { } // verify the entry matches - if (!entry.matches(data.request.getUri(), data.request.getMethod(), data.request.getHeaders().getHeaders().toMultimap())) { + if (!entry.matches(data.request.getUri(), data.request.getMethod(), data.request.getHeaders().toMultimap())) { networkCount++; StreamUtility.closeQuietly(snapshot); return null; @@ -153,7 +154,8 @@ public Cancellable getSocket(final GetSocketData data) { cachedResponseHeaders.setLocalTimestamps(System.currentTimeMillis(), System.currentTimeMillis()); long now = System.currentTimeMillis(); - ResponseSource responseSource = cachedResponseHeaders.chooseResponseSource(now, data.request.getHeaders()); + RequestHeaders requestHeaders = new RequestHeaders(null, data.request.getHeaders()); + ResponseSource responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders); if (responseSource == ResponseSource.CACHE) { data.request.logi("Response retrieved from cache"); @@ -211,19 +213,20 @@ public int getCacheStoreCount() { public void onBodyDecoder(OnBodyData data) { CachedSocket cached = com.koushikdutta.async.Util.getWrappedSocket(data.socket, CachedSocket.class); if (cached != null) { - data.headers.getHeaders().set(SERVED_FROM, CACHE); + data.headers.set(SERVED_FROM, CACHE); return; } CacheData cacheData = data.state.get("cache-data"); if (cacheData != null) { - if (cacheData.cachedResponseHeaders.validate(data.headers)) { + ResponseHeaders networkResponse = new ResponseHeaders(null, data.headers); + if (cacheData.cachedResponseHeaders.validate(networkResponse)) { data.request.logi("Serving response from conditional cache"); - data.headers.getHeaders().removeAll("Content-Length"); - data.headers = cacheData.cachedResponseHeaders.combine(data.headers); - data.headers.getHeaders().setStatusLine(cacheData.cachedResponseHeaders.getHeaders().getStatusLine()); + data.headers.removeAll("Content-Length"); + data.headers = cacheData.cachedResponseHeaders.combine(networkResponse).getHeaders(); + data.headers.setStatusLine(cacheData.cachedResponseHeaders.getHeaders().getStatusLine()); - data.headers.getHeaders().set(SERVED_FROM, CONDITIONAL_CACHE); + data.headers.set(SERVED_FROM, CONDITIONAL_CACHE); conditionalCacheHitCount++; CachedBodyEmitter bodySpewer = new CachedBodyEmitter(cacheData.candidate, cacheData.contentLength); @@ -241,7 +244,7 @@ public void onBodyDecoder(OnBodyData data) { if (!caching) return; - if (!data.headers.isCacheable(data.request.getHeaders()) || !data.request.getMethod().equals(AsyncHttpGet.METHOD)) { + if (!HttpUtil.isCacheable(data.request.getHeaders(), data.headers) || !data.request.getMethod().equals(AsyncHttpGet.METHOD)) { /* * Don't cache non-GET responses. We're technically allowed to cache * HEAD requests and some POST requests, but the complexity of doing @@ -253,7 +256,7 @@ public void onBodyDecoder(OnBodyData data) { } String key = FileCache.toKeyString(data.request.getUri()); - RawHeaders varyHeaders = data.request.getHeaders().getHeaders().getAll(data.headers.getVaryFields()); + RawHeaders varyHeaders = data.request.getHeaders().getAll(HttpUtil.varyFields(data.headers)); Entry entry = new Entry(data.request.getUri(), varyHeaders, data.request, data.headers); BodyCacher cacher = new BodyCacher(); EntryEditor editor = new EntryEditor(key); @@ -554,11 +557,11 @@ public Entry(InputStream in) throws IOException { } } - public Entry(Uri uri, RawHeaders varyHeaders, AsyncHttpRequest request, ResponseHeaders responseHeaders) { + public Entry(Uri uri, RawHeaders varyHeaders, AsyncHttpRequest request, RawHeaders responseHeaders) { this.uri = uri.toString(); this.varyHeaders = varyHeaders; this.requestMethod = request.getMethod(); - this.responseHeaders = responseHeaders.getHeaders(); + this.responseHeaders = responseHeaders; // if (isHttps()) { // HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java index 7e0770690..dba645a02 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java @@ -11,22 +11,22 @@ public Cancellable getSocket(GetSocketData data) { @Override public void onSocket(OnSocketData data) { - } @Override public void onHeadersReceived(OnHeadersReceivedData data) { - } @Override public void onBodyDecoder(OnBodyData data) { - } @Override public void onRequestComplete(OnRequestCompleteData data) { - } + @Override + public boolean sendHeaders(SendHeaderData data) { + return false; + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java index eda555615..dd38e5ec7 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java @@ -131,7 +131,7 @@ public WebSocketImpl(AsyncHttpServerRequest request, AsyncHttpServerResponse res } public static void addWebSocketUpgradeHeaders(AsyncHttpRequest req, String protocol) { - RawHeaders headers = req.getHeaders().getHeaders(); + RawHeaders headers = req.getHeaders(); final String key = Base64.encodeToString(toByteArray(UUID.randomUUID()),Base64.NO_WRAP); headers.set("Sec-WebSocket-Version", "13"); headers.set("Sec-WebSocket-Key", key); @@ -142,8 +142,8 @@ public static void addWebSocketUpgradeHeaders(AsyncHttpRequest req, String proto headers.set("Sec-WebSocket-Protocol", protocol); headers.set("Pragma", "no-cache"); headers.set("Cache-Control", "no-cache"); - if (TextUtils.isEmpty(req.getHeaders().getUserAgent())) - req.getHeaders().setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.15 Safari/537.36"); + if (TextUtils.isEmpty(req.getHeaders().get("User-Agent"))) + req.getHeaders().set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.15 Safari/537.36"); } public WebSocketImpl(AsyncSocket socket) { @@ -154,12 +154,12 @@ public WebSocketImpl(AsyncSocket socket) { public static WebSocket finishHandshake(RawHeaders requestHeaders, AsyncHttpResponse response) { if (response == null) return null; - if (response.getHeaders().getHeaders().getResponseCode() != 101) + if (response.headers().getResponseCode() != 101) return null; - if (!"websocket".equalsIgnoreCase(response.getHeaders().getHeaders().get("Upgrade"))) + if (!"websocket".equalsIgnoreCase(response.headers().get("Upgrade"))) return null; - String sha1 = response.getHeaders().getHeaders().get("Sec-WebSocket-Accept"); + String sha1 = response.headers().get("Sec-WebSocket-Accept"); if (sha1 == null) return null; String key = requestHeaders.get("Sec-WebSocket-Key"); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java b/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java new file mode 100644 index 000000000..80f1be67e --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java @@ -0,0 +1,10 @@ +package com.koushikdutta.async.http.callback; + +import com.koushikdutta.async.http.libcore.RawHeaders; + +/** + * Created by koush on 6/30/13. + */ +public interface HeadersCallback { + public void onHeaders(RawHeaders headers); +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java index a91be67b2..4b9c9eac0 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java @@ -26,6 +26,8 @@ public interface CacheControlHandler { * Parse a comma-separated list of cache control header values. */ public static void parseCacheControl(String value, CacheControlHandler handler) { + if (value == null) + return; int pos = 0; while (pos < value.length()) { int tokenStart = pos; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java index 59e492925..3ac9da1a2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java @@ -69,6 +69,8 @@ public final class HttpDate { * parsed. */ public static Date parse(String value) { + if (value == null) + return null; try { return STANDARD_DATE_FORMAT.get().parse(value); } catch (ParseException ignore) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/HttpTransport.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/HttpTransport.java new file mode 100644 index 000000000..0d017bba6 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/HttpTransport.java @@ -0,0 +1,15 @@ +package com.koushikdutta.async.http.spdy; + +import com.koushikdutta.async.http.AsyncHttpClientMiddleware; +import com.koushikdutta.async.http.SimpleMiddleware; + +/** + * Created by koush on 7/19/14. + */ +public class HttpTransport extends SimpleMiddleware { + @Override + public boolean sendHeaders(SendHeaderData data) { + return super.sendHeaders(data); + } + +} diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java index 6809880ec..ea9832951 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java @@ -115,7 +115,7 @@ public void testClockworkMod() throws Exception { @Override public void onConnectCompleted(Exception ex, AsyncHttpResponse response) { // make sure gzip decoding works, as that is generally what github sends. - Assert.assertEquals("gzip", response.getHeaders().getContentEncoding()); + Assert.assertEquals("gzip", response.headers().get("Content-Encoding")); response.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java b/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java index ebaae9ddd..ac4471316 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java @@ -43,8 +43,8 @@ public void onCompleted(Exception ex) { AsyncHttpGet get = new AsyncHttpGet("http://localhost:5959/"); get.setLogging("issue59", Log.VERBOSE); - get.getHeaders().getHeaders().removeAll("Connection"); - get.getHeaders().getHeaders().removeAll("Accept-Encoding"); + get.getHeaders().removeAll("Connection"); + get.getHeaders().removeAll("Accept-Encoding"); assertEquals("foobarbeepboop", AsyncHttpClient.getDefaultInstance().executeString(get, null).get(1000, TimeUnit.MILLISECONDS)); } From 5b950f69eda8e7a7c7d611fbe06cc3aed2c0bae7 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 21 Jul 2014 14:37:01 -0700 Subject: [PATCH 032/399] more libcore removal --- .../koushikdutta/async/http/WebSocketImpl.java | 6 +++--- .../async/http/server/AsyncHttpServer.java | 12 ++++++------ .../http/server/AsyncHttpServerRequest.java | 6 +++--- .../http/server/AsyncHttpServerRequestImpl.java | 17 +++++------------ .../server/AsyncHttpServerResponseImpl.java | 4 ++-- 5 files changed, 19 insertions(+), 26 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java index dd38e5ec7..bfcbab6b9 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java @@ -110,16 +110,16 @@ protected void sendFrame(byte[] frame) { public WebSocketImpl(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { this(request.getSocket()); - String key = request.getHeaders().getHeaders().get("Sec-WebSocket-Key"); + String key = request.getHeaders().get("Sec-WebSocket-Key"); String concat = key + MAGIC; String sha1 = SHA1(concat); - String origin = request.getHeaders().getHeaders().get("Origin"); + String origin = request.getHeaders().get("Origin"); response.responseCode(101); response.getHeaders().getHeaders().set("Upgrade", "WebSocket"); response.getHeaders().getHeaders().set("Connection", "Upgrade"); response.getHeaders().getHeaders().set("Sec-WebSocket-Accept", sha1); - String protocol = request.getHeaders().getHeaders().get("Sec-WebSocket-Protocol"); + String protocol = request.getHeaders().get("Sec-WebSocket-Protocol"); // match the protocol (sanity checking and enforcement is done in the caller) if (!TextUtils.isEmpty(protocol)) response.getHeaders().getHeaders().set("Sec-WebSocket-Protocol", protocol); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java index b1b393d64..b5d69e695 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java @@ -79,7 +79,7 @@ protected AsyncHttpRequestBody onUnknownBody(RawHeaders headers) { @Override protected void onHeadersReceived() { - RawHeaders headers = getRawHeaders(); + RawHeaders headers = getHeaders(); // should the negotiation of 100 continue be here, or in the request impl? // probably here, so AsyncResponse can negotiate a 100 continue. @@ -173,7 +173,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { private void handleOnCompleted() { if (requestComplete && responseComplete) { - if (HttpUtil.isKeepAlive(getHeaders().getHeaders())) { + if (HttpUtil.isKeepAlive(getHeaders())) { onAccepted(socket); } else { @@ -285,7 +285,7 @@ public void addAction(String action, String regex, HttpServerRequestCallback cal } public static interface WebSocketRequestCallback { - public void onConnected(WebSocket webSocket, RequestHeaders headers); + public void onConnected(WebSocket webSocket, RawHeaders headers); } public void websocket(String regex, final WebSocketRequestCallback callback) { @@ -297,7 +297,7 @@ public void websocket(String regex, final String protocol, final WebSocketReques @Override public void onRequest(final AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { boolean hasUpgrade = false; - String connection = request.getHeaders().getHeaders().get("Connection"); + String connection = request.getHeaders().get("Connection"); if (connection != null) { String[] connections = connection.split(","); for (String c: connections) { @@ -307,12 +307,12 @@ public void onRequest(final AsyncHttpServerRequest request, final AsyncHttpServe } } } - if (!"websocket".equalsIgnoreCase(request.getHeaders().getHeaders().get("Upgrade")) || !hasUpgrade) { + if (!"websocket".equalsIgnoreCase(request.getHeaders().get("Upgrade")) || !hasUpgrade) { response.responseCode(404); response.end(); return; } - String peerProtocol = request.getHeaders().getHeaders().get("Sec-WebSocket-Protocol"); + String peerProtocol = request.getHeaders().get("Sec-WebSocket-Protocol"); if (!TextUtils.equals(protocol, peerProtocol)) { response.responseCode(404); response.end(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java index be7d8884b..9af204e6a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java @@ -2,14 +2,14 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; -import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.http.Multimap; -import com.koushikdutta.async.http.libcore.RequestHeaders; +import com.koushikdutta.async.http.body.AsyncHttpRequestBody; +import com.koushikdutta.async.http.libcore.RawHeaders; import java.util.regex.Matcher; public interface AsyncHttpServerRequest extends DataEmitter { - public RequestHeaders getHeaders(); + public RawHeaders getHeaders(); public Matcher getMatcher(); public AsyncHttpRequestBody getBody(); public AsyncSocket getSocket(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java index c2e2c1323..3e82bd3f2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java @@ -1,7 +1,5 @@ package com.koushikdutta.async.http.server; -import java.util.regex.Matcher; - import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; @@ -9,10 +7,11 @@ import com.koushikdutta.async.LineEmitter.StringCallback; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; -import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.http.HttpUtil; +import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.http.libcore.RawHeaders; -import com.koushikdutta.async.http.libcore.RequestHeaders; + +import java.util.regex.Matcher; public abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter implements AsyncHttpServerRequest, CompletedCallback { private RawHeaders mRawHeaders = new RawHeaders(); @@ -68,7 +67,6 @@ else if (!"\r".equals(s)){ mBody = new UnknownRequestBody(mRawHeaders.get("Content-Type")); } mBody.parse(emitter, mReporter); - mHeaders = new RequestHeaders(null, mRawHeaders); onHeadersReceived(); } } @@ -78,10 +76,6 @@ else if (!"\r".equals(s)){ } }; - RawHeaders getRawHeaders() { - return mRawHeaders; - } - String method; @Override public String getMethod() { @@ -101,10 +95,9 @@ public AsyncSocket getSocket() { return mSocket; } - private RequestHeaders mHeaders; @Override - public RequestHeaders getHeaders() { - return mHeaders; + public RawHeaders getHeaders() { + return mRawHeaders; } @Override diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java index d1cb9527f..08d3af9cc 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java @@ -45,7 +45,7 @@ public AsyncSocket getSocket() { AsyncHttpServerResponseImpl(AsyncSocket socket, AsyncHttpServerRequestImpl req) { mSocket = socket; mRequest = req; - if (HttpUtil.isKeepAlive(req.getHeaders().getHeaders())) + if (HttpUtil.isKeepAlive(req.getHeaders())) mRawHeaders.set("Connection", "Keep-Alive"); } @@ -204,7 +204,7 @@ public void sendStream(final InputStream inputStream, long totalLength) { long start = 0; long end = totalLength - 1; - String range = mRequest.getHeaders().getHeaders().get("Range"); + String range = mRequest.getHeaders().get("Range"); if (range != null) { String[] parts = range.split("="); if (parts.length != 2 || !"bytes".equals(parts[0])) { From 54ef12d411c1d251b02fd9deed8d175c8c97679e Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 21 Jul 2014 20:17:31 -0700 Subject: [PATCH 033/399] Move all cache related stuff into subpackage. Contain libcore. --- .../async/http/AsyncHttpClient.java | 2 +- .../async/http/AsyncHttpClientMiddleware.java | 2 +- .../async/http/AsyncHttpRequest.java | 3 +- .../async/http/AsyncHttpResponse.java | 2 +- .../async/http/AsyncHttpResponseImpl.java | 5 +- .../async/http/AsyncSSLSocketMiddleware.java | 2 +- .../async/http/{libcore => }/HttpDate.java | 2 +- .../com/koushikdutta/async/http/HttpUtil.java | 29 +--- .../com/koushikdutta/async/http/Multimap.java | 2 +- .../async/http/WebSocketImpl.java | 22 +-- .../http/body/MultipartFormDataBody.java | 2 +- .../koushikdutta/async/http/body/Part.java | 3 +- .../async/http/cache/CacheUtil.java | 35 +++++ .../http/{libcore => cache}/HeaderParser.java | 2 +- .../http/{libcore => cache}/Objects.java | 4 +- .../http/{libcore => cache}/RawHeaders.java | 2 +- .../{libcore => cache}/RequestHeaders.java | 6 +- .../{ => cache}/ResponseCacheMiddleware.java | 20 +-- .../{libcore => cache}/ResponseHeaders.java | 7 +- .../{libcore => cache}/ResponseSource.java | 4 +- .../{libcore => cache}/StrictLineReader.java | 4 +- .../async/http/callback/HeadersCallback.java | 2 +- .../async/http/filter/GZIPInputFilter.java | 22 ++- .../async/http/libcore/Memory.java | 126 ------------------ .../async/http/server/AsyncHttpServer.java | 13 +- .../http/server/AsyncHttpServerRequest.java | 2 +- .../server/AsyncHttpServerRequestImpl.java | 2 +- .../http/server/AsyncHttpServerResponse.java | 14 +- .../server/AsyncHttpServerResponseImpl.java | 15 +-- .../koushikdutta/async/test/CacheTests.java | 8 +- .../async/test/HttpClientTests.java | 2 +- .../async/test/HttpServerTests.java | 4 +- .../com/koushikdutta/async/test/Issue59.java | 2 +- .../async/test/WebSocketTests.java | 4 +- 34 files changed, 131 insertions(+), 245 deletions(-) rename AndroidAsync/src/com/koushikdutta/async/http/{libcore => }/HttpDate.java (98%) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/cache/CacheUtil.java rename AndroidAsync/src/com/koushikdutta/async/http/{libcore => cache}/HeaderParser.java (98%) rename AndroidAsync/src/com/koushikdutta/async/http/{libcore => cache}/Objects.java (92%) rename AndroidAsync/src/com/koushikdutta/async/http/{libcore => cache}/RawHeaders.java (99%) rename AndroidAsync/src/com/koushikdutta/async/http/{libcore => cache}/RequestHeaders.java (98%) rename AndroidAsync/src/com/koushikdutta/async/http/{ => cache}/ResponseCacheMiddleware.java (97%) rename AndroidAsync/src/com/koushikdutta/async/http/{libcore => cache}/ResponseHeaders.java (99%) rename AndroidAsync/src/com/koushikdutta/async/http/{libcore => cache}/ResponseSource.java (93%) rename AndroidAsync/src/com/koushikdutta/async/http/{libcore => cache}/StrictLineReader.java (98%) delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/libcore/Memory.java diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index 7f66deb0b..b6654a9a6 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -20,7 +20,7 @@ import com.koushikdutta.async.http.AsyncHttpClientMiddleware.OnRequestCompleteData; import com.koushikdutta.async.http.callback.HttpConnectCallback; import com.koushikdutta.async.http.callback.RequestCallback; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.parser.AsyncParser; import com.koushikdutta.async.parser.ByteBufferListParser; import com.koushikdutta.async.parser.JSONArrayParser; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java index 79deb4c7b..240448276 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java @@ -5,7 +5,7 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.future.Cancellable; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.util.UntypedHashtable; public interface AsyncHttpClientMiddleware { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java index b3eb45b52..fa50c20f7 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java @@ -5,8 +5,7 @@ import com.koushikdutta.async.AsyncSSLException; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; -import com.koushikdutta.async.http.libcore.RawHeaders; -import com.koushikdutta.async.http.libcore.RequestHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import org.apache.http.Header; import org.apache.http.HeaderIterator; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java index 631879c99..f44ba8eea 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java @@ -3,7 +3,7 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.CompletedCallback; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; public interface AsyncHttpResponse extends DataEmitter { public void setEndCallback(CompletedCallback handler); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index a3e4189c6..e1b23998f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -13,11 +13,8 @@ import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.http.filter.ChunkedOutputFilter; -import com.koushikdutta.async.http.libcore.RawHeaders; -import com.koushikdutta.async.http.libcore.ResponseHeaders; -import com.koushikdutta.async.util.Charsets; +import com.koushikdutta.async.http.cache.RawHeaders; -import java.nio.ByteBuffer; import java.nio.charset.Charset; abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements AsyncSocket, AsyncHttpResponse { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java index b339c935e..9e03461c6 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java @@ -10,7 +10,7 @@ import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import java.io.IOException; import java.util.ArrayList; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java b/AndroidAsync/src/com/koushikdutta/async/http/HttpDate.java similarity index 98% rename from AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java rename to AndroidAsync/src/com/koushikdutta/async/http/HttpDate.java index 3ac9da1a2..f4e79c311 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HttpDate.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/HttpDate.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.koushikdutta.async.http.libcore; +package com.koushikdutta.async.http; import java.text.DateFormat; import java.text.ParseException; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java b/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java index c30e42b0d..bbcdf8fc6 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java @@ -9,18 +9,11 @@ import com.koushikdutta.async.http.body.MultipartFormDataBody; import com.koushikdutta.async.http.body.StringBody; import com.koushikdutta.async.http.body.UrlEncodedFormBody; +import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.http.filter.ChunkedInputFilter; import com.koushikdutta.async.http.filter.ContentLengthFilter; import com.koushikdutta.async.http.filter.GZIPInputFilter; import com.koushikdutta.async.http.filter.InflaterInputFilter; -import com.koushikdutta.async.http.libcore.RawHeaders; -import com.koushikdutta.async.http.libcore.RequestHeaders; -import com.koushikdutta.async.http.libcore.ResponseHeaders; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; public class HttpUtil { public static AsyncHttpRequestBody getBody(DataEmitter emitter, CompletedCallback reporter, RawHeaders headers) { @@ -148,24 +141,4 @@ public static int contentLength(RawHeaders headers) { return -1; } } - - public static Set varyFields(RawHeaders headers) { - HashSet ret = new HashSet(); - String value = headers.get("Vary"); - if (value == null) - return ret; - for (String varyField : value.split(",")) { - ret.add(varyField.trim()); - } - return ret; - } - - public static boolean isCacheable(RawHeaders requestHeaders, RawHeaders responseHeaders) { - ResponseHeaders r = new ResponseHeaders(null, responseHeaders); - return r.isCacheable(new RequestHeaders(null, requestHeaders)); - } - - public static boolean isNoCache(RawHeaders headers) { - return new RequestHeaders(null, headers).isNoCache(); - } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java b/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java index 628bdd036..efc16a850 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java @@ -1,7 +1,7 @@ package com.koushikdutta.async.http; import android.net.Uri; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import org.apache.http.NameValuePair; import org.apache.http.message.BasicNameValuePair; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java index bfcbab6b9..4af0b9961 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java @@ -1,11 +1,5 @@ package com.koushikdutta.async.http; -import java.nio.ByteBuffer; -import java.nio.LongBuffer; -import java.security.MessageDigest; -import java.util.LinkedList; -import java.util.UUID; - import android.text.TextUtils; import android.util.Base64; @@ -17,10 +11,16 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; +import java.nio.ByteBuffer; +import java.nio.LongBuffer; +import java.security.MessageDigest; +import java.util.LinkedList; +import java.util.UUID; + public class WebSocketImpl implements WebSocket { @Override public void end() { @@ -116,13 +116,13 @@ public WebSocketImpl(AsyncHttpServerRequest request, AsyncHttpServerResponse res String origin = request.getHeaders().get("Origin"); response.responseCode(101); - response.getHeaders().getHeaders().set("Upgrade", "WebSocket"); - response.getHeaders().getHeaders().set("Connection", "Upgrade"); - response.getHeaders().getHeaders().set("Sec-WebSocket-Accept", sha1); + response.getHeaders().set("Upgrade", "WebSocket"); + response.getHeaders().set("Connection", "Upgrade"); + response.getHeaders().set("Sec-WebSocket-Accept", sha1); String protocol = request.getHeaders().get("Sec-WebSocket-Protocol"); // match the protocol (sanity checking and enforcement is done in the caller) if (!TextUtils.isEmpty(protocol)) - response.getHeaders().getHeaders().set("Sec-WebSocket-Protocol", protocol); + response.getHeaders().set("Sec-WebSocket-Protocol", protocol); // if (origin != null) // response.getHeaders().getHeaders().set("Access-Control-Allow-Origin", "http://" + origin); response.writeHead(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java b/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java index c61a9d96b..1872e19c8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java @@ -12,7 +12,7 @@ import com.koushikdutta.async.future.Continuation; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.Multimap; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.http.server.BoundaryEmitter; import java.io.File; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/body/Part.java b/AndroidAsync/src/com/koushikdutta/async/http/body/Part.java index cfe499361..56fcecb58 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/body/Part.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/body/Part.java @@ -3,12 +3,11 @@ import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.Multimap; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import org.apache.http.NameValuePair; import java.io.File; import java.util.List; -import java.util.Map; public class Part { public static final String CONTENT_DISPOSITION = "Content-Disposition"; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/cache/CacheUtil.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/CacheUtil.java new file mode 100644 index 000000000..caadf8f30 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/CacheUtil.java @@ -0,0 +1,35 @@ +package com.koushikdutta.async.http.cache; + +import java.util.HashSet; +import java.util.Set; + +/** + * Created by koush on 7/21/14. + */ +class CacheUtil { + static Set varyFields(RawHeaders headers) { + HashSet ret = new HashSet(); + String value = headers.get("Vary"); + if (value == null) + return ret; + for (String varyField : value.split(",")) { + ret.add(varyField.trim()); + } + return ret; + } + + static boolean isCacheable(RawHeaders requestHeaders, RawHeaders responseHeaders) { + ResponseHeaders r = new ResponseHeaders(null, responseHeaders); + return r.isCacheable(new RequestHeaders(null, requestHeaders)); + } + + static boolean isNoCache(RawHeaders headers) { + return new RequestHeaders(null, headers).isNoCache(); + } + + ResponseSource chooseResponseSource(long nowMillis, RawHeaders request, RawHeaders response) { + RequestHeaders requestHeaders = new RequestHeaders(null, request); + ResponseHeaders responseHeaders = new ResponseHeaders(null, response); + return responseHeaders.chooseResponseSource(nowMillis, requestHeaders); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/HeaderParser.java similarity index 98% rename from AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java rename to AndroidAsync/src/com/koushikdutta/async/http/cache/HeaderParser.java index 4b9c9eac0..4c0a8f1aa 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/HeaderParser.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/HeaderParser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.koushikdutta.async.http.libcore; +package com.koushikdutta.async.http.cache; final class HeaderParser { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/Objects.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/Objects.java similarity index 92% rename from AndroidAsync/src/com/koushikdutta/async/http/libcore/Objects.java rename to AndroidAsync/src/com/koushikdutta/async/http/cache/Objects.java index a0bfbad3e..23e5afd94 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/Objects.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/Objects.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.koushikdutta.async.http.libcore; +package com.koushikdutta.async.http.cache; -public final class Objects { +final class Objects { private Objects() {} /** diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/RawHeaders.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/RawHeaders.java similarity index 99% rename from AndroidAsync/src/com/koushikdutta/async/http/libcore/RawHeaders.java rename to AndroidAsync/src/com/koushikdutta/async/http/cache/RawHeaders.java index cb446be86..53be34091 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/RawHeaders.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/RawHeaders.java @@ -1,4 +1,4 @@ -package com.koushikdutta.async.http.libcore; +package com.koushikdutta.async.http.cache; /* * Licensed to the Apache Software Foundation (ASF) under one or more diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/RequestHeaders.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/RequestHeaders.java similarity index 98% rename from AndroidAsync/src/com/koushikdutta/async/http/libcore/RequestHeaders.java rename to AndroidAsync/src/com/koushikdutta/async/http/cache/RequestHeaders.java index b5180b40c..15768605c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/RequestHeaders.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/RequestHeaders.java @@ -14,10 +14,12 @@ * limitations under the License. */ -package com.koushikdutta.async.http.libcore; +package com.koushikdutta.async.http.cache; import android.net.Uri; +import com.koushikdutta.async.http.HttpDate; + import java.util.Date; import java.util.List; import java.util.Map; @@ -25,7 +27,7 @@ /** * Parsed HTTP request headers. */ -public final class RequestHeaders { +final class RequestHeaders { private final Uri uri; private final RawHeaders headers; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java similarity index 97% rename from AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java rename to AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java index a81167905..22f0ac2a3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java @@ -1,4 +1,4 @@ -package com.koushikdutta.async.http; +package com.koushikdutta.async.http.cache; import android.net.Uri; import android.util.Base64; @@ -14,13 +14,13 @@ import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.SimpleCancellable; -import com.koushikdutta.async.http.libcore.RequestHeaders; -import com.koushikdutta.async.util.Charsets; -import com.koushikdutta.async.http.libcore.RawHeaders; -import com.koushikdutta.async.http.libcore.ResponseHeaders; -import com.koushikdutta.async.http.libcore.ResponseSource; -import com.koushikdutta.async.http.libcore.StrictLineReader; +import com.koushikdutta.async.http.AsyncHttpClient; +import com.koushikdutta.async.http.AsyncHttpClientMiddleware; +import com.koushikdutta.async.http.AsyncHttpGet; +import com.koushikdutta.async.http.AsyncHttpRequest; +import com.koushikdutta.async.http.SimpleMiddleware; import com.koushikdutta.async.util.Allocator; +import com.koushikdutta.async.util.Charsets; import com.koushikdutta.async.util.FileCache; import com.koushikdutta.async.util.StreamUtility; @@ -95,7 +95,7 @@ public void setCaching(boolean caching) { // also see if this can be turned into a conditional cache request. @Override public Cancellable getSocket(final GetSocketData data) { - if (cache == null || !caching || HttpUtil.isNoCache(data.request.getHeaders())) { + if (cache == null || !caching || CacheUtil.isNoCache(data.request.getHeaders())) { networkCount++; return null; } @@ -244,7 +244,7 @@ public void onBodyDecoder(OnBodyData data) { if (!caching) return; - if (!HttpUtil.isCacheable(data.request.getHeaders(), data.headers) || !data.request.getMethod().equals(AsyncHttpGet.METHOD)) { + if (!CacheUtil.isCacheable(data.request.getHeaders(), data.headers) || !data.request.getMethod().equals(AsyncHttpGet.METHOD)) { /* * Don't cache non-GET responses. We're technically allowed to cache * HEAD requests and some POST requests, but the complexity of doing @@ -256,7 +256,7 @@ public void onBodyDecoder(OnBodyData data) { } String key = FileCache.toKeyString(data.request.getUri()); - RawHeaders varyHeaders = data.request.getHeaders().getAll(HttpUtil.varyFields(data.headers)); + RawHeaders varyHeaders = data.request.getHeaders().getAll(CacheUtil.varyFields(data.headers)); Entry entry = new Entry(data.request.getUri(), varyHeaders, data.request, data.headers); BodyCacher cacher = new BodyCacher(); EntryEditor editor = new EntryEditor(key); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseHeaders.java similarity index 99% rename from AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java rename to AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseHeaders.java index 3cc128ecf..327564714 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseHeaders.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseHeaders.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package com.koushikdutta.async.http.libcore; +package com.koushikdutta.async.http.cache; import android.net.Uri; +import com.koushikdutta.async.http.HttpDate; + import java.net.HttpURLConnection; -import java.net.URI; import java.util.Collections; import java.util.Date; import java.util.List; @@ -31,7 +32,7 @@ /** * Parsed HTTP response headers. */ -public final class ResponseHeaders { +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"; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseSource.java similarity index 93% rename from AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java rename to AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseSource.java index 3501b0ff5..a4046c710 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/ResponseSource.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseSource.java @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.koushikdutta.async.http.libcore; +package com.koushikdutta.async.http.cache; -public enum ResponseSource { +enum ResponseSource { /** * Return the response from the cache immediately. diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/StrictLineReader.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/StrictLineReader.java similarity index 98% rename from AndroidAsync/src/com/koushikdutta/async/http/libcore/StrictLineReader.java rename to AndroidAsync/src/com/koushikdutta/async/http/cache/StrictLineReader.java index d1cb5d964..35b8306f0 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/StrictLineReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/StrictLineReader.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.koushikdutta.async.http.libcore; +package com.koushikdutta.async.http.cache; import com.koushikdutta.async.util.Charsets; @@ -43,7 +43,7 @@ * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1. * The default charset is US_ASCII. */ -public class StrictLineReader implements Closeable { +class StrictLineReader implements Closeable { private static final byte CR = (byte)'\r'; private static final byte LF = (byte)'\n'; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java b/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java index 80f1be67e..68db69f2a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java @@ -1,6 +1,6 @@ package com.koushikdutta.async.http.callback; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; /** * Created by koush on 6/30/13. diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java index e1a23d014..83beef7f7 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java @@ -1,9 +1,11 @@ package com.koushikdutta.async.http.filter; -import com.koushikdutta.async.*; -import com.koushikdutta.async.callback.DataCallback; -import com.koushikdutta.async.http.libcore.Memory; +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.NullDataCallback; +import com.koushikdutta.async.PushParser; import com.koushikdutta.async.PushParser.ParseCallback; +import com.koushikdutta.async.callback.DataCallback; import java.io.IOException; import java.nio.ByteBuffer; @@ -13,6 +15,14 @@ import java.util.zip.Inflater; public class GZIPInputFilter extends InflaterInputFilter { + static short peekShort(byte[] src, int offset, ByteOrder order) { + if (order == ByteOrder.BIG_ENDIAN) { + return (short) ((src[offset] << 8) | (src[offset + 1] & 0xff)); + } else { + return (short) ((src[offset + 1] << 8) | (src[offset] & 0xff)); + } + } + private static final int FCOMMENT = 16; private static final int FEXTRA = 4; @@ -44,7 +54,7 @@ public void onDataAvailable(final DataEmitter emitter, ByteBufferList bb) { boolean hcrc; public void parsed(byte[] header) { - short magic = Memory.peekShort(header, 0, ByteOrder.LITTLE_ENDIAN); + short magic = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN); if (magic != (short) GZIPInputStream.GZIP_MAGIC) { report(new IOException(String.format("unknown format (magic number %x)", magic))); emitter.setDataCallback(new NullDataCallback()); @@ -61,7 +71,7 @@ public void parsed(byte[] header) { if (hcrc) { crc.update(header, 0, 2); } - int length = Memory.peekShort(header, 0, ByteOrder.LITTLE_ENDIAN) & 0xffff; + int length = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN) & 0xffff; parser.readByteArray(length, new ParseCallback() { public void parsed(byte[] buf) { if (hcrc) { @@ -100,7 +110,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (hcrc) { parser.readByteArray(2, new ParseCallback() { public void parsed(byte[] header) { - short crc16 = Memory.peekShort(header, 0, ByteOrder.LITTLE_ENDIAN); + short crc16 = peekShort(header, 0, ByteOrder.LITTLE_ENDIAN); if ((short) crc.getValue() != crc16) { report(new IOException("CRC mismatch")); return; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/libcore/Memory.java b/AndroidAsync/src/com/koushikdutta/async/http/libcore/Memory.java deleted file mode 100644 index 8b3b2811d..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/libcore/Memory.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.koushikdutta.async.http.libcore; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; - -/** - * Unsafe access to memory. - */ -public final class Memory { - private Memory() { } - - public static int peekInt(byte[] src, int offset, ByteOrder order) { - if (order == ByteOrder.BIG_ENDIAN) { - return (((src[offset++] & 0xff) << 24) | - ((src[offset++] & 0xff) << 16) | - ((src[offset++] & 0xff) << 8) | - ((src[offset ] & 0xff) << 0)); - } else { - return (((src[offset++] & 0xff) << 0) | - ((src[offset++] & 0xff) << 8) | - ((src[offset++] & 0xff) << 16) | - ((src[offset ] & 0xff) << 24)); - } - } - - public static long peekLong(byte[] src, int offset, ByteOrder order) { - if (order == ByteOrder.BIG_ENDIAN) { - int h = ((src[offset++] & 0xff) << 24) | - ((src[offset++] & 0xff) << 16) | - ((src[offset++] & 0xff) << 8) | - ((src[offset++] & 0xff) << 0); - int l = ((src[offset++] & 0xff) << 24) | - ((src[offset++] & 0xff) << 16) | - ((src[offset++] & 0xff) << 8) | - ((src[offset ] & 0xff) << 0); - return (((long) h) << 32L) | ((long) l) & 0xffffffffL; - } else { - int l = ((src[offset++] & 0xff) << 0) | - ((src[offset++] & 0xff) << 8) | - ((src[offset++] & 0xff) << 16) | - ((src[offset++] & 0xff) << 24); - int h = ((src[offset++] & 0xff) << 0) | - ((src[offset++] & 0xff) << 8) | - ((src[offset++] & 0xff) << 16) | - ((src[offset ] & 0xff) << 24); - return (((long) h) << 32L) | ((long) l) & 0xffffffffL; - } - } - - public static short peekShort(byte[] src, int offset, ByteOrder order) { - if (order == ByteOrder.BIG_ENDIAN) { - return (short) ((src[offset] << 8) | (src[offset + 1] & 0xff)); - } else { - return (short) ((src[offset + 1] << 8) | (src[offset] & 0xff)); - } - } - - public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) { - if (order == ByteOrder.BIG_ENDIAN) { - dst[offset++] = (byte) ((value >> 24) & 0xff); - dst[offset++] = (byte) ((value >> 16) & 0xff); - dst[offset++] = (byte) ((value >> 8) & 0xff); - dst[offset ] = (byte) ((value >> 0) & 0xff); - } else { - dst[offset++] = (byte) ((value >> 0) & 0xff); - dst[offset++] = (byte) ((value >> 8) & 0xff); - dst[offset++] = (byte) ((value >> 16) & 0xff); - dst[offset ] = (byte) ((value >> 24) & 0xff); - } - } - - public static void pokeLong(byte[] dst, int offset, long value, ByteOrder order) { - if (order == ByteOrder.BIG_ENDIAN) { - int i = (int) (value >> 32); - dst[offset++] = (byte) ((i >> 24) & 0xff); - dst[offset++] = (byte) ((i >> 16) & 0xff); - dst[offset++] = (byte) ((i >> 8) & 0xff); - dst[offset++] = (byte) ((i >> 0) & 0xff); - i = (int) value; - dst[offset++] = (byte) ((i >> 24) & 0xff); - dst[offset++] = (byte) ((i >> 16) & 0xff); - dst[offset++] = (byte) ((i >> 8) & 0xff); - dst[offset ] = (byte) ((i >> 0) & 0xff); - } else { - int i = (int) value; - dst[offset++] = (byte) ((i >> 0) & 0xff); - dst[offset++] = (byte) ((i >> 8) & 0xff); - dst[offset++] = (byte) ((i >> 16) & 0xff); - dst[offset++] = (byte) ((i >> 24) & 0xff); - i = (int) (value >> 32); - dst[offset++] = (byte) ((i >> 0) & 0xff); - dst[offset++] = (byte) ((i >> 8) & 0xff); - dst[offset++] = (byte) ((i >> 16) & 0xff); - dst[offset ] = (byte) ((i >> 24) & 0xff); - } - } - - public static void pokeShort(byte[] dst, int offset, short value, ByteOrder order) { - if (order == ByteOrder.BIG_ENDIAN) { - dst[offset++] = (byte) ((value >> 8) & 0xff); - dst[offset ] = (byte) ((value >> 0) & 0xff); - } else { - dst[offset++] = (byte) ((value >> 0) & 0xff); - dst[offset ] = (byte) ((value >> 8) & 0xff); - } - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java index b5d69e695..1c981b796 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java @@ -25,8 +25,7 @@ import com.koushikdutta.async.http.WebSocket; import com.koushikdutta.async.http.WebSocketImpl; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; -import com.koushikdutta.async.http.libcore.RawHeaders; -import com.koushikdutta.async.http.libcore.RequestHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.util.StreamUtility; import java.io.File; @@ -150,7 +149,7 @@ else if (requestComplete) { @Override public void onCompleted(Exception e) { // if the protocol was switched off http, ignore this request/response. - if (res.getHeaders().getHeaders().getResponseCode() == 101) + if (res.getHeaders().getResponseCode() == 101) return; requestComplete = true; super.onCompleted(e); @@ -381,14 +380,14 @@ public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerRespo String path = request.getMatcher().replaceAll(""); android.util.Pair pair = getAssetStream(_context, assetPath + path); final InputStream is = pair.second; - response.getHeaders().getHeaders().set("Content-Length", String.valueOf(pair.first)); + response.getHeaders().set("Content-Length", String.valueOf(pair.first)); if (is == null) { response.responseCode(404); response.end(); return; } response.responseCode(200); - response.getHeaders().getHeaders().add("Content-Type", getContentType(assetPath + path)); + response.getHeaders().add("Content-Type", getContentType(assetPath + path)); Util.pump(is, response, new CompletedCallback() { @Override public void onCompleted(Exception ex) { @@ -405,14 +404,14 @@ public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerRespo android.util.Pair pair = getAssetStream(_context, assetPath + path); final InputStream is = pair.second; StreamUtility.closeQuietly(is); - response.getHeaders().getHeaders().set("Content-Length", String.valueOf(pair.first)); + response.getHeaders().set("Content-Length", String.valueOf(pair.first)); if (is == null) { response.responseCode(404); response.end(); return; } response.responseCode(200); - response.getHeaders().getHeaders().add("Content-Type", getContentType(assetPath + path)); + response.getHeaders().add("Content-Type", getContentType(assetPath + path)); response.writeHead(); response.end(); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java index 9af204e6a..77ce87545 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java @@ -4,7 +4,7 @@ import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import java.util.regex.Matcher; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java index 3e82bd3f2..ba0576fd3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java @@ -9,7 +9,7 @@ import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.HttpUtil; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; -import com.koushikdutta.async.http.libcore.RawHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import java.util.regex.Matcher; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java index 07d8a92d8..87fbc5e2e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java @@ -1,14 +1,14 @@ package com.koushikdutta.async.http.server; -import java.io.File; -import java.io.InputStream; - -import org.json.JSONObject; - import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; -import com.koushikdutta.async.http.libcore.ResponseHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; + +import org.json.JSONObject; + +import java.io.File; +import java.io.InputStream; public interface AsyncHttpServerResponse extends DataSink, CompletedCallback { public void end(); @@ -18,7 +18,7 @@ public interface AsyncHttpServerResponse extends DataSink, CompletedCallback { public void sendFile(File file); public void sendStream(InputStream inputStream, long totalLength); public void responseCode(int code); - public ResponseHeaders getHeaders(); + public RawHeaders getHeaders(); public void writeHead(); public void setContentType(String contentType); public void redirect(String location); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java index 08d3af9cc..0fe4e1001 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java @@ -12,9 +12,8 @@ import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.AsyncHttpHead; import com.koushikdutta.async.http.HttpUtil; +import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.http.filter.ChunkedOutputFilter; -import com.koushikdutta.async.http.libcore.RawHeaders; -import com.koushikdutta.async.http.libcore.ResponseHeaders; import com.koushikdutta.async.util.StreamUtility; import org.json.JSONObject; @@ -24,16 +23,14 @@ import java.io.FileInputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; public class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { private RawHeaders mRawHeaders = new RawHeaders(); private long mContentLength = -1; - private ResponseHeaders mHeaders = new ResponseHeaders(null, mRawHeaders); - + @Override - public ResponseHeaders getHeaders() { - return mHeaders; + public RawHeaders getHeaders() { + return mRawHeaders; } public AsyncSocket getSocket() { @@ -226,7 +223,7 @@ public void sendStream(final InputStream inputStream, long totalLength) { end = totalLength - 1; responseCode(206); - getHeaders().getHeaders().set("Content-Range", String.format("bytes %d-%d/%d", start, end, totalLength)); + getHeaders().set("Content-Range", String.format("bytes %d-%d/%d", start, end, totalLength)); } catch (Exception e) { responseCode(416); @@ -240,7 +237,7 @@ public void sendStream(final InputStream inputStream, long totalLength) { mContentLength = end - start + 1; mRawHeaders.set("Content-Length", String.valueOf(mContentLength)); mRawHeaders.set("Accept-Ranges", "bytes"); - if (getHeaders().getHeaders().getStatusLine() == null) + if (getHeaders().getStatusLine() == null) responseCode(200); if (mRequest.getMethod().equals(AsyncHttpHead.METHOD)) { writeHead(); diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/CacheTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/CacheTests.java index bffc34cb6..78f330126 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/CacheTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/CacheTests.java @@ -6,8 +6,8 @@ import com.koushikdutta.async.AsyncServerSocket; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpGet; -import com.koushikdutta.async.http.ResponseCacheMiddleware; -import com.koushikdutta.async.http.libcore.HttpDate; +import com.koushikdutta.async.http.HttpDate; +import com.koushikdutta.async.http.cache.ResponseCacheMiddleware; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; @@ -30,8 +30,8 @@ public void testMaxAgePrivate() throws Exception { httpServer.get("/uname/(.*)", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { - response.getHeaders().getHeaders().set("Date", HttpDate.format(new Date())); - response.getHeaders().getHeaders().set("Cache-Control", "private, max-age=10000"); + response.getHeaders().set("Date", HttpDate.format(new Date())); + response.getHeaders().set("Cache-Control", "private, max-age=10000"); response.send(request.getMatcher().group(1)); } }); diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java index ea9832951..b9645e407 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java @@ -20,7 +20,7 @@ import com.koushikdutta.async.http.AsyncHttpPost; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.AsyncHttpResponse; -import com.koushikdutta.async.http.ResponseCacheMiddleware; +import com.koushikdutta.async.http.cache.ResponseCacheMiddleware; import com.koushikdutta.async.http.body.JSONObjectBody; import com.koushikdutta.async.http.callback.HttpConnectCallback; import com.koushikdutta.async.http.server.AsyncHttpServer; diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpServerTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpServerTests.java index df117e64f..7139cafce 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpServerTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpServerTests.java @@ -49,7 +49,7 @@ public void onCompleted(Exception ex) { httpServer.get("/hello", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { - assertNotNull(request.getHeaders().getHost()); + assertNotNull(request.getHeaders().get("Host")); response.send("hello"); } }); @@ -58,7 +58,7 @@ public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse re @Override public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { try { - assertNotNull(request.getHeaders().getHost()); + assertNotNull(request.getHeaders().get("Host")); JSONObject json = new JSONObject(); if (request.getBody() instanceof UrlEncodedFormBody) { UrlEncodedFormBody body = (UrlEncodedFormBody)request.getBody(); diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java b/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java index ac4471316..a72f306c3 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java @@ -28,7 +28,7 @@ public void testIssue() throws Exception { public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { // setting this to empty is a hacky way of telling the framework not to use // transfer-encoding. It will get removed. - response.getHeaders().getHeaders().set("Transfer-Encoding", ""); + response.getHeaders().set("Transfer-Encoding", ""); response.responseCode(200); Util.writeAll(response, "foobarbeepboop".getBytes(), new CompletedCallback() { @Override diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/WebSocketTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/WebSocketTests.java index a5a7f92b0..dcbe6c810 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/WebSocketTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/WebSocketTests.java @@ -6,7 +6,7 @@ import com.koushikdutta.async.http.AsyncHttpClient.WebSocketConnectCallback; import com.koushikdutta.async.http.WebSocket; import com.koushikdutta.async.http.WebSocket.StringCallback; -import com.koushikdutta.async.http.libcore.RequestHeaders; +import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServer.WebSocketRequestCallback; @@ -34,7 +34,7 @@ public void onCompleted(Exception ex) { httpServer.websocket("/ws", new WebSocketRequestCallback() { @Override - public void onConnected(final WebSocket webSocket, RequestHeaders headers) { + public void onConnected(final WebSocket webSocket, RawHeaders headers) { webSocket.setStringCallback(new StringCallback() { @Override public void onStringAvailable(String s) { From 68c948e0748fca9b8a9caff76953cb6b3d26db7f Mon Sep 17 00:00:00 2001 From: Steve Lhomme Date: Tue, 22 Jul 2014 08:04:32 +0200 Subject: [PATCH 034/399] Crash fix for a case where the EndCallback is not set and used --- .../src/com/koushikdutta/async/BufferedDataEmitter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java b/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java index 009f6c6bd..5c88b310f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java +++ b/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java @@ -32,7 +32,7 @@ public void onDataAvailable() { if (mDataCallback != null && !mPaused && mBuffers.remaining() > 0) mDataCallback.onDataAvailable(this, mBuffers); - if (mEnded && mBuffers.remaining() == 0) + if (mEnded && mBuffers.remaining() == 0 && mEndCallback != null) mEndCallback.onCompleted(mEndException); } From 11b629da6c967fe6b3c588c0d77dbc0fdc53b4c5 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 21 Jul 2014 23:32:53 -0700 Subject: [PATCH 035/399] wip header rewrite --- .../com/koushikdutta/async/http/Headers.java | 61 +++++++++++++++++++ .../com/koushikdutta/async/http/Multimap.java | 5 +- 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/Headers.java diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Headers.java b/AndroidAsync/src/com/koushikdutta/async/http/Headers.java new file mode 100644 index 000000000..8a2146b24 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/Headers.java @@ -0,0 +1,61 @@ +package com.koushikdutta.async.http; + +import java.util.List; + +/** + * Created by koush on 7/21/14. + */ +public class Headers { + Multimap map = new Multimap(); + public Multimap getMultiMap() { + return map; + } + + public List getAll(String header) { + return map.get(header); + } + + public String get(String header) { + return map.getString(header); + } + + public Headers set(String header, String value) { + map.put(header, value); + return this; + } + + public Headers add(String header, String value) { + map.add(header, value); + return this; + } + + public List remove(String header) { + return map.remove(header); + } + + int responseCode; + public int getResponseCode() { + return responseCode; + } + public void setResponseCode(int responseCode) { + this.responseCode = responseCode; + } + + String protocol; + public String getProtocol() { + return protocol; + } + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + String responseMessage; + + public String getResponseMessage() { + return responseMessage; + } + + public void setResponseMessage(String responseMessage) { + this.responseMessage = responseMessage; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java b/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java index efc16a850..5223fc850 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java @@ -1,7 +1,9 @@ package com.koushikdutta.async.http; import android.net.Uri; + import com.koushikdutta.async.http.cache.RawHeaders; + import org.apache.http.NameValuePair; import org.apache.http.message.BasicNameValuePair; @@ -9,12 +11,13 @@ import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; /** * Created by koush on 5/27/13. */ -public class Multimap extends Hashtable> implements Iterable { +public class Multimap extends LinkedHashMap> implements Iterable { public Multimap() { } From 1b08bd6375fca37e5bd41169c2e80b55f0c1f0b3 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 23 Jul 2014 01:06:25 -0700 Subject: [PATCH 036/399] All libcore usage hidden. Fix obscure buffering/replay bugs in SSL. --- AndroidAsync/AndroidManifest.xml | 4 +- .../async/AsyncSSLSocketWrapper.java | 33 +++++- .../com/koushikdutta/async/AsyncServer.java | 7 +- .../koushikdutta/async/DataEmitterBase.java | 3 - .../async/FilteredDataEmitter.java | 6 +- .../src/com/koushikdutta/async/Util.java | 1 - .../async/http/AsyncHttpClient.java | 19 ++- .../async/http/AsyncHttpClientMiddleware.java | 55 +++++++-- .../async/http/AsyncHttpRequest.java | 40 +++---- .../async/http/AsyncHttpResponse.java | 8 +- .../async/http/AsyncHttpResponseImpl.java | 67 ++++++++--- .../async/http/AsyncSSLSocketMiddleware.java | 111 ++++++++---------- .../async/http/AsyncSocketMiddleware.java | 17 ++- .../com/koushikdutta/async/http/Headers.java | 111 +++++++++++++++--- .../com/koushikdutta/async/http/HttpUtil.java | 30 ++--- .../com/koushikdutta/async/http/Multimap.java | 79 ++++++------- .../com/koushikdutta/async/http/Protocol.java | 89 ++++++++++++++ .../async/http/SimpleMiddleware.java | 7 +- .../async/http/WebSocketImpl.java | 9 +- .../http/body/MultipartFormDataBody.java | 17 ++- .../koushikdutta/async/http/body/Part.java | 15 +-- .../async/http/cache/CacheUtil.java | 35 ------ .../async/http/cache/RawHeaders.java | 2 +- .../http/cache/ResponseCacheMiddleware.java | 27 +++-- .../async/http/callback/HeadersCallback.java | 10 -- .../http/filter/ContentLengthFilter.java | 2 +- .../async/http/server/AsyncHttpServer.java | 82 +++++++------ .../http/server/AsyncHttpServerRequest.java | 4 +- .../server/AsyncHttpServerRequestImpl.java | 32 +++-- .../http/server/AsyncHttpServerResponse.java | 7 +- .../server/AsyncHttpServerResponseImpl.java | 55 +++++---- .../async/http/server/AsyncProxyServer.java | 81 +++++++++++++ .../async/http/spdy/AsyncSpdyConnection.java | 8 +- .../async/http/spdy/SpdyMiddleware.java | 2 +- .../async/test/HttpClientTests.java | 19 +-- .../com/koushikdutta/async/test/Issue59.java | 2 +- .../koushikdutta/async/test/OkHttpTest.java | 2 +- .../async/test/WebSocketTests.java | 4 +- 38 files changed, 706 insertions(+), 396 deletions(-) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/Protocol.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/cache/CacheUtil.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/server/AsyncProxyServer.java diff --git a/AndroidAsync/AndroidManifest.xml b/AndroidAsync/AndroidManifest.xml index 6984a72e7..bd080cd06 100644 --- a/AndroidAsync/AndroidManifest.xml +++ b/AndroidAsync/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="200" + android:versionName="2.0.0"> diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 7f609994a..84d7da1c1 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -115,6 +115,10 @@ public void onCompleted(Exception ex) { } } + boolean mEnded; + Exception mEndException; + final ByteBufferList transformed = new ByteBufferList(); + private AsyncSSLSocketWrapper(AsyncSocket socket, String host, int port, SSLEngine sslEngine, @@ -140,10 +144,20 @@ public void onWriteable() { // SSL needs buffering of data written during handshake. // aka exhcange.setDatacallback mEmitter = new BufferedDataEmitter(socket); + mEmitter.setEndCallback(new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + if (mEnded) + return; + mEnded = true; + mEndException = ex; + if (!transformed.hasRemaining() && mEndCallback != null) + mEndCallback.onCompleted(ex); + } + }); final Allocator allocator = new Allocator(); allocator.setMinAlloc(8192); - final ByteBufferList transformed = new ByteBufferList(); mEmitter.setDataCallback(new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { @@ -195,7 +209,7 @@ else if (res.getStatus() == Status.BUFFER_UNDERFLOW) { } } - Util.emitAllData(AsyncSSLSocketWrapper.this, transformed); + AsyncSSLSocketWrapper.this.onDataAvailable(); } catch (SSLException ex) { ex.printStackTrace(); @@ -208,6 +222,14 @@ else if (res.getStatus() == Status.BUFFER_UNDERFLOW) { }); } + public void onDataAvailable() { + Util.emitAllData(this, transformed); + + if (mEnded && !transformed.hasRemaining()) + mEndCallback.onCompleted(mEndException); + } + + @Override public SSLEngine getSSLEngine() { return engine; @@ -432,14 +454,15 @@ public CompletedCallback getClosedCallback() { return mSocket.getClosedCallback(); } + CompletedCallback mEndCallback; @Override public void setEndCallback(CompletedCallback callback) { - mSocket.setEndCallback(callback); + mEndCallback = callback; } @Override public CompletedCallback getEndCallback() { - return mSocket.getEndCallback(); + return mEndCallback; } @Override @@ -449,6 +472,8 @@ public void pause() { @Override public void resume() { + onDataAvailable(); + mEmitter.onDataAvailable(); mSocket.resume(); } diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java index a1c7a476a..b399362ca 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java @@ -126,7 +126,12 @@ private static void wakeup(final SelectorWrapper selector) { synchronousWorkers.execute(new Runnable() { @Override public void run() { - selector.wakeupOnce(); + try { + selector.wakeupOnce(); + } + catch (Exception e) { + Log.i(LOGTAG, "Selector shit the bed."); + } } }); } diff --git a/AndroidAsync/src/com/koushikdutta/async/DataEmitterBase.java b/AndroidAsync/src/com/koushikdutta/async/DataEmitterBase.java index 90e05c725..1c05617a4 100644 --- a/AndroidAsync/src/com/koushikdutta/async/DataEmitterBase.java +++ b/AndroidAsync/src/com/koushikdutta/async/DataEmitterBase.java @@ -8,9 +8,6 @@ */ public abstract class DataEmitterBase implements DataEmitter { private boolean ended; - protected void resetEnded() { - ended = false; - } protected void report(Exception e) { if (ended) return; diff --git a/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java b/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java index 71bcfa940..6c59def72 100644 --- a/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java +++ b/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java @@ -5,7 +5,7 @@ import com.koushikdutta.async.wrapper.DataEmitterWrapper; public class FilteredDataEmitter extends DataEmitterBase implements DataEmitter, DataCallback, DataEmitterWrapper, DataTrackingEmitter { - DataEmitter mEmitter; + private DataEmitter mEmitter; @Override public DataEmitter getDataEmitter() { return mEmitter; @@ -41,8 +41,8 @@ public void setDataTracker(DataTracker tracker) { this.tracker = tracker; } - DataTracker tracker; - int totalRead; + private DataTracker tracker; + private int totalRead; @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { if (bb != null) diff --git a/AndroidAsync/src/com/koushikdutta/async/Util.java b/AndroidAsync/src/com/koushikdutta/async/Util.java index 18c3736cc..c8df5b77f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/Util.java @@ -131,7 +131,6 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { sink.setWriteableCallback(new WritableCallback() { @Override public void onWriteable() { - dataCallback.onDataAvailable(emitter, new ByteBufferList()); emitter.resume(); } }); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index b6654a9a6..313a9fa10 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -20,7 +20,6 @@ import com.koushikdutta.async.http.AsyncHttpClientMiddleware.OnRequestCompleteData; import com.koushikdutta.async.http.callback.HttpConnectCallback; import com.koushikdutta.async.http.callback.RequestCallback; -import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.parser.AsyncParser; import com.koushikdutta.async.parser.ByteBufferListParser; import com.koushikdutta.async.parser.JSONArrayParser; @@ -209,6 +208,12 @@ private void executeAffinity(final AsyncHttpRequest request, final int redirectC request.logd("Executing request."); + synchronized (mMiddleware) { + for (AsyncHttpClientMiddleware middleware: mMiddleware) { + middleware.onRequest(data); + } + } + // flow: // 1) set a connect timeout // 2) wait for connect @@ -252,12 +257,6 @@ public void onConnectCompleted(Exception ex, AsyncSocket socket) { mServer.removeAllCallbacks(cancel.scheduled); data.socket = socket; - synchronized (mMiddleware) { - for (AsyncHttpClientMiddleware middleware: mMiddleware) { - middleware.onSocket(data); - } - } - cancel.socket = socket; if (ex != null) { @@ -294,7 +293,7 @@ public void setDataEmitter(DataEmitter emitter) { super.setDataEmitter(data.bodyEmitter); - RawHeaders headers = mHeaders; + Headers headers = mHeaders; int responseCode = code(); if ((responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == 307) && request.getFollowRedirect()) { String location = headers.get("Location"); @@ -327,7 +326,7 @@ public void setDataEmitter(DataEmitter emitter) { return; } - request.logv("Final (post cache response) headers:\n" + mHeaders.toHeaderString()); + request.logv("Final (post cache response) headers:\n" + toString()); // at this point the headers are done being modified reportConnectedCompleted(cancel, null, this, request, callback); @@ -343,7 +342,7 @@ protected void onHeadersReceived() { mServer.removeAllCallbacks(cancel.scheduled); // allow the middleware to massage the headers before the body is decoded - request.logv("Received headers:\n" + mHeaders.toHeaderString()); + request.logv("Received headers:\n" + toString()); data.headers = mHeaders; synchronized (mMiddleware) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java index 240448276..c706e536f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java @@ -5,27 +5,31 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.future.Cancellable; -import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.util.UntypedHashtable; +/** + * AsyncHttpClientMiddleware is used by AsyncHttpClient to + * inspect, manipulate, and handle http requests. + */ public interface AsyncHttpClientMiddleware { - public static class GetSocketData { + public static class OnRequestData { public UntypedHashtable state = new UntypedHashtable(); public AsyncHttpRequest request; + } + + public static class GetSocketData extends OnRequestData { public ConnectCallback connectCallback; public Cancellable socketCancellable; - } - - public static class OnSocketData extends GetSocketData { - public AsyncSocket socket; + public String protocol; } - public static class SendHeaderData extends OnSocketData { - CompletedCallback sendHeadersCallback; + public static class SendHeaderData extends GetSocketData { + public AsyncSocket socket; + public CompletedCallback sendHeadersCallback; } public static class OnHeadersReceivedData extends SendHeaderData { - public RawHeaders headers; + public Headers headers; } public static class OnBodyData extends OnHeadersReceivedData { @@ -37,10 +41,41 @@ public static class OnRequestCompleteData extends OnBodyData { public Exception exception; } + /** + * Called immediately upon request execution + * @param data + */ + public void onRequest(OnRequestData data); + + /** + * Called to retrieve the socket that will fulfill this request + * @param data + * @return + */ public Cancellable getSocket(GetSocketData data); - public void onSocket(OnSocketData data); + + /** + * Called before the headers are sent via the socket + * @param data + * @return + */ public boolean sendHeaders(SendHeaderData data); + + /** + * Called once the headers have been received via the socket + * @param data + */ public void onHeadersReceived(OnHeadersReceivedData data); + + /** + * Called before the body is decoded + * @param data + */ public void onBodyDecoder(OnBodyData data); + + /** + * Called once the request is complete + * @param data + */ public void onRequestComplete(OnRequestCompleteData data); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java index fa50c20f7..95dd21d1a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java @@ -5,7 +5,6 @@ import com.koushikdutta.async.AsyncSSLException; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; -import com.koushikdutta.async.http.cache.RawHeaders; import org.apache.http.Header; import org.apache.http.HeaderIterator; @@ -35,7 +34,7 @@ public ProtocolVersion getProtocolVersion() { public String getMethod() { return mMethod; } - + @Override public String toString() { String path = AsyncHttpRequest.this.getUri().getEncodedPath(); @@ -88,7 +87,6 @@ public AsyncHttpRequest setMethod(String method) { if (getClass() != AsyncHttpRequest.class) throw new UnsupportedOperationException("can't change method on a subclass of AsyncHttpRequest"); mMethod = method; - mRawHeaders.setStatusLine(getRequestLine().toString()); return this; } @@ -96,7 +94,7 @@ public AsyncHttpRequest(Uri uri, String method) { this(uri, method, null); } - public static void setDefaultHeaders(RawHeaders ret, Uri uri) { + public static void setDefaultHeaders(Headers ret, Uri uri) { if (uri != null) { String host = uri.getHost(); if (uri.getPort() != -1) @@ -110,17 +108,16 @@ public static void setDefaultHeaders(RawHeaders ret, Uri uri) { ret.set("Accept", "*/*"); } - public AsyncHttpRequest(Uri uri, String method, RawHeaders headers) { + public AsyncHttpRequest(Uri uri, String method, Headers headers) { assert uri != null; mMethod = method; this.uri = uri; if (headers == null) - mRawHeaders = new RawHeaders(); + mRawHeaders = new Headers(); else mRawHeaders = headers; if (headers == null) setDefaultHeaders(mRawHeaders, uri); - mRawHeaders.setStatusLine(getRequestLine().toString()); } Uri uri; @@ -128,16 +125,12 @@ public Uri getUri() { return uri; } - private RawHeaders mRawHeaders = new RawHeaders(); + private Headers mRawHeaders = new Headers(); - public RawHeaders getHeaders() { + public Headers getHeaders() { return mRawHeaders; } - public String getRequestString() { - return mRawHeaders.toHeaderString(); - } - private boolean mFollowRedirect = true; public boolean getFollowRedirect() { return mFollowRedirect; @@ -208,13 +201,7 @@ public boolean containsHeader(String name) { @Override public Header[] getAllHeaders() { - Header[] ret = new Header[request.getHeaders().length()]; - for (int i = 0; i < ret.length; i++) { - String name = request.getHeaders().getFieldName(i); - String value = request.getHeaders().getValue(i); - ret[i] = new BasicHeader(name, value); - } - return ret; + return request.getHeaders().toHeaderArray(); } @Override @@ -227,7 +214,7 @@ public Header getFirstHeader(String name) { @Override public Header[] getHeaders(String name) { - Map> map = request.getHeaders().toMultimap(); + Map> map = request.getHeaders().getMultiMap(); List vals = map.get(name); if (vals == null) return new Header[0]; @@ -270,12 +257,12 @@ public HeaderIterator headerIterator(String name) { @Override public void removeHeader(Header header) { - request.getHeaders().removeAll(header.getName()); + request.getHeaders().remove(header.getName()); } @Override public void removeHeaders(String name) { - request.getHeaders().removeAll(name); + request.getHeaders().remove(name); } @Override @@ -334,6 +321,13 @@ public int getProxyPort() { return proxyPort; } + @Override + public String toString() { + if (mRawHeaders == null) + return super.toString(); + return mRawHeaders.toPrefixString(uri.toString()); + } + public void setLogging(String tag, int level) { LOGTAG = tag; logLevel = level; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java index f44ba8eea..e7713952c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java @@ -3,15 +3,15 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.CompletedCallback; -import com.koushikdutta.async.http.cache.RawHeaders; public interface AsyncHttpResponse extends DataEmitter { - public void setEndCallback(CompletedCallback handler); public String protocol(); public String message(); public int code(); - public RawHeaders headers(); - public void end(); + public AsyncHttpResponse protocol(String protocol); + public AsyncHttpResponse message(String message); + public AsyncHttpResponse code(int code); + public Headers headers(); public AsyncSocket detachSocket(); public AsyncHttpRequest getRequest(); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index e1b23998f..fca047354 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -9,12 +9,13 @@ import com.koushikdutta.async.LineEmitter; import com.koushikdutta.async.LineEmitter.StringCallback; import com.koushikdutta.async.NullDataCallback; +import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.http.filter.ChunkedOutputFilter; -import com.koushikdutta.async.http.cache.RawHeaders; +import java.io.IOException; import java.nio.charset.Charset; abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements AsyncSocket, AsyncHttpResponse { @@ -60,9 +61,10 @@ public void onCompleted(Exception ex) { } }); - String rs = mRequest.getRequestString(); + String rl = mRequest.getRequestLine().toString(); + String rs = mRequest.getHeaders().toPrefixString(rl); mRequest.logv("\n" + rs); - com.koushikdutta.async.Util.writeAll(exchange, rs.getBytes(), new CompletedCallback() { + Util.writeAll(exchange, rs.getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (mWriter != null) { @@ -72,8 +74,7 @@ public void onCompleted(Exception ex) { onRequestCompleted(ex); } }); - } - else { + } else { onRequestCompleted(null); } } @@ -102,17 +103,25 @@ public void onCompleted(Exception error) { protected abstract void onHeadersReceived(); StringCallback mHeaderCallback = new StringCallback() { - private RawHeaders mRawHeaders = new RawHeaders(); + private Headers mRawHeaders = new Headers(); + private String statusLine; @Override public void onStringAvailable(String s) { try { - if (mRawHeaders.getStatusLine() == null) { - mRawHeaders.setStatusLine(s); + if (statusLine == null) { + statusLine = s; } else if (!"\r".equals(s)) { mRawHeaders.addLine(s); } else { + String[] parts = statusLine.split(" ", 3); + if (parts.length != 3) + throw new Exception(new IOException("Not HTTP")); + + protocol = parts[0]; + code = Integer.parseInt(parts[1]); + message = parts[2]; mHeaders = mRawHeaders; onHeadersReceived(); // socket may get detached after headers (websocket) @@ -125,7 +134,7 @@ else if (!"\r".equals(s)) { emitter = HttpUtil.EndEmitter.create(getServer(), null); } else { - emitter = HttpUtil.getBodyDecoder(mSocket, mRawHeaders, false); + emitter = HttpUtil.getBodyDecoder(mSocket, Protocol.get(protocol), mHeaders, false); } setDataEmitter(emitter); } @@ -158,7 +167,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { private AsyncHttpRequest mRequest; private AsyncSocket mSocket; - protected RawHeaders mHeaders; + protected Headers mHeaders; public AsyncHttpResponseImpl(AsyncHttpRequest request) { mRequest = request; } @@ -166,23 +175,51 @@ public AsyncHttpResponseImpl(AsyncHttpRequest request) { boolean mCompleted = false; @Override - public RawHeaders headers() { + public Headers headers() { return mHeaders; } + int code; @Override public int code() { - return headers().getResponseCode(); + return code; + } + + @Override + public AsyncHttpResponse code(int code) { + this.code = code; + return this; } + @Override + public AsyncHttpResponse protocol(String protocol) { + this.protocol = protocol; + return this; + } + + @Override + public AsyncHttpResponse message(String message) { + this.message = message; + return this; + } + + String protocol; @Override public String protocol() { - return "HTTP/1." + headers().getHttpMinorVersion(); + return protocol; } + String message; @Override public String message() { - return headers().getResponseMessage(); + return message; + } + + @Override + public String toString() { + if (mHeaders == null) + return super.toString(); + return mHeaders.toPrefixString(protocol + " " + code + " " + message); } private boolean mFirstWrite = true; @@ -240,7 +277,7 @@ public AsyncServer getServer() { @Override public String charset() { - Multimap mm = Multimap.parseHeader(headers(), "Content-Type"); + Multimap mm = Multimap.parseSemicolonDelimited(headers().get("Content-Type")); String cs; if (mm != null && null != (cs = mm.getString("charset")) && Charset.isSupported(cs)) { return cs; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java index 4ec7d804b..59fa0f7c6 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java @@ -10,7 +10,6 @@ import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; -import com.koushikdutta.async.http.cache.RawHeaders; import java.io.IOException; import java.util.ArrayList; @@ -86,75 +85,69 @@ protected void tryHandshake(final ConnectCallback callback, AsyncSocket socket, } @Override - protected ConnectCallback wrapCallback(final ConnectCallback callback, final Uri uri, final int port, final boolean proxied) { + protected ConnectCallback wrapCallback(GetSocketData data, final Uri uri, final int port, final boolean proxied, final ConnectCallback callback) { return new ConnectCallback() { @Override public void onConnectCompleted(Exception ex, final AsyncSocket socket) { - if (ex == null) { - if (!proxied) { - tryHandshake(callback, socket, uri, port); - } - else { - // this SSL connection is proxied, must issue a CONNECT request to the proxy server - // http://stackoverflow.com/a/6594880/704837 - RawHeaders connect = new RawHeaders(); - connect.setStatusLine(String.format("CONNECT %s:%s HTTP/1.1", uri.getHost(), port)); - Util.writeAll(socket, connect.toHeaderString().getBytes(), new CompletedCallback() { + if (ex != null) { + callback.onConnectCompleted(ex, socket); + return; + } + + if (!proxied) { + tryHandshake(callback, socket, uri, port); + return; + } + + // this SSL connection is proxied, must issue a CONNECT request to the proxy server + // http://stackoverflow.com/a/6594880/704837 + String connect = String.format("CONNECT %s:%s HTTP/1.1\r\n\r\n", uri.getHost(), port); + Util.writeAll(socket, connect.getBytes(), new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + if (ex != null) { + callback.onConnectCompleted(ex, socket); + return; + } + + LineEmitter liner = new LineEmitter(); + liner.setLineCallback(new LineEmitter.StringCallback() { + String statusLine; @Override - public void onCompleted(Exception ex) { - if (ex != null) { - callback.onConnectCompleted(ex, socket); - return; + public void onStringAvailable(String s) { + if (statusLine == null) { + statusLine = s; + if (statusLine.length() > 128 || !statusLine.contains("200")) { + socket.setDataCallback(null); + socket.setEndCallback(null); + callback.onConnectCompleted(new IOException("non 200 status line"), socket); + } } - - LineEmitter liner = new LineEmitter(); - liner.setLineCallback(new LineEmitter.StringCallback() { - String statusLine; - @Override - public void onStringAvailable(String s) { - if (statusLine == null) { - statusLine = s; - if (statusLine.length() > 128 || !statusLine.contains("200")) { - socket.setDataCallback(null); - socket.setEndCallback(null); - callback.onConnectCompleted(new IOException("non 200 status line"), socket); - } - } - else { - socket.setDataCallback(null); - socket.setEndCallback(null); - if (TextUtils.isEmpty(s.trim())) { - tryHandshake(callback, socket, uri, port); - } - else { - callback.onConnectCompleted(new IOException("unknown second status line"), socket); - } - } + else { + socket.setDataCallback(null); + socket.setEndCallback(null); + if (TextUtils.isEmpty(s.trim())) { + tryHandshake(callback, socket, uri, port); } - }); - - socket.setDataCallback(liner); - - socket.setEndCallback(new CompletedCallback() { - @Override - public void onCompleted(Exception ex) { - if (!socket.isOpen() && ex == null) - ex = new IOException("socket closed before proxy connect response"); - callback.onConnectCompleted(ex, socket); + else { + callback.onConnectCompleted(new IOException("unknown second status line"), socket); } - }); + } + } + }); + + socket.setDataCallback(liner); -// AsyncSocket wrapper = socket; -// if (ex == null) -// wrapper = new AsyncSSLSocketWrapper(socket, uri.getHost(), port, sslContext, trustManagers, hostnameVerifier, true); -// callback.onConnectCompleted(ex, wrapper); + socket.setEndCallback(new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + if (!socket.isOpen() && ex == null) + ex = new IOException("socket closed before proxy connect response"); + callback.onConnectCompleted(ex, socket); } }); } - } - else { - callback.onConnectCompleted(ex, socket); - } + }); } }; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java index 6e7d6927a..37ffe5cc6 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java @@ -52,7 +52,7 @@ public AsyncSocketMiddleware(AsyncHttpClient client) { protected AsyncHttpClient mClient; - protected ConnectCallback wrapCallback(ConnectCallback callback, Uri uri, int port, boolean proxied) { + protected ConnectCallback wrapCallback(GetSocketData data, Uri uri, int port, boolean proxied, ConnectCallback callback) { return callback; } @@ -170,22 +170,19 @@ public Cancellable getSocket(final GetSocketData data) { if (data.request.getProxyHost() != null) { unresolvedHost = data.request.getProxyHost(); unresolvedPort = data.request.getProxyPort(); - // set the host and port explicitly for proxied connections - data.request.getHeaders().setStatusLine(data.request.getProxyRequestLine().toString()); proxied = true; } else if (proxyHost != null) { unresolvedHost = proxyHost; unresolvedPort = proxyPort; - // set the host and port explicitly for proxied connections - data.request.getHeaders().setStatusLine(data.request.getProxyRequestLine().toString()); proxied = true; } else { unresolvedHost = uri.getHost(); unresolvedPort = port; } - return mClient.getServer().connectSocket(unresolvedHost, unresolvedPort, wrapCallback(data.connectCallback, uri, port, proxied)); + return mClient.getServer().connectSocket(unresolvedHost, unresolvedPort, + wrapCallback(data, uri, port, proxied, data.connectCallback)); } // try to connect to everything... @@ -216,7 +213,8 @@ public void onCompleted(Exception ex) { keepTrying.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, final CompletedCallback next) throws Exception { - mClient.getServer().connectSocket(new InetSocketAddress(address, port), wrapCallback(new ConnectCallback() { + mClient.getServer().connectSocket(new InetSocketAddress(address, port), + wrapCallback(data, uri, port, false, new ConnectCallback() { @Override public void onConnectCompleted(Exception ex, AsyncSocket socket) { if (isDone()) { @@ -244,7 +242,7 @@ public void onConnectCompleted(Exception ex, AsyncSocket socket) { data.connectCallback.onConnectCompleted(ex, socket); } } - }, uri, port, false)); + })); } }); } @@ -360,7 +358,8 @@ public void onRequestComplete(final OnRequestCompleteData data) { data.socket.close(); return; } - if (!HttpUtil.isKeepAlive(data.headers) || !HttpUtil.isKeepAlive(data.request.getHeaders())) { + if (!HttpUtil.isKeepAlive(data.response.protocol(), data.headers) + || !HttpUtil.isKeepAlive(Protocol.HTTP_1_1, data.request.getHeaders())) { data.request.logv("closing out socket (not keep alive)"); data.socket.close(); return; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Headers.java b/AndroidAsync/src/com/koushikdutta/async/http/Headers.java index 8a2146b24..8fb70253e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/Headers.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/Headers.java @@ -1,11 +1,28 @@ package com.koushikdutta.async.http; + +import android.text.TextUtils; + +import com.koushikdutta.async.http.server.AsyncHttpServer; + +import org.apache.http.Header; +import org.apache.http.message.BasicHeader; + +import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Created by koush on 7/21/14. */ public class Headers { + public Headers() { + } + + public Headers(Map> mm) { + map.putAll(mm); + } + Multimap map = new Multimap(); public Multimap getMultiMap() { return map; @@ -29,33 +46,93 @@ public Headers add(String header, String value) { return this; } - public List remove(String header) { - return map.remove(header); + public Headers addLine(String line) { + if (line != null) { + line = line.trim(); + String[] parts = line.split(":", 2); + if (parts.length == 2) + add(parts[0].trim(), parts[1].trim()); + else + add(parts[0].trim(), ""); + } + return this; } - int responseCode; - public int getResponseCode() { - return responseCode; + public Headers addAll(String header, List values) { + for (String v: values) { + add(header, v); + } + return this; } - public void setResponseCode(int responseCode) { - this.responseCode = responseCode; + + public Headers addAll(Map> m) { + map.putAll(m); + return this; } - String protocol; - public String getProtocol() { - return protocol; + public Headers addAll(Headers headers) { + map.putAll(headers.map); + return this; + } + + public List removeAll(String header) { + return map.remove(header); + } + + public String remove(String header) { + List r = removeAll(header); + if (r == null || r.size() == 0) + return null; + return r.get(0); } - public void setProtocol(String protocol) { - this.protocol = protocol; + + public Header[] toHeaderArray() { + ArrayList
ret = new ArrayList
(); + for (String key: map.keySet()) { + for (String v: map.get(key)) { + ret.add(new BasicHeader(key, v)); + } + } + return ret.toArray(new Header[ret.size()]); + } + + public StringBuilder toStringBuilder() { + StringBuilder result = new StringBuilder(256); + for (String key: map.keySet()) { + for (String v: map.get(key)) { + result.append(key) + .append(": ") + .append(v) + .append("\r\n"); + } + } + result.append("\r\n"); + return result; } - String responseMessage; + @Override + public String toString() { + return toStringBuilder().toString(); + } - public String getResponseMessage() { - return responseMessage; + public String toPrefixString(String prefix) { + return + toStringBuilder() + .insert(0, prefix + "\r\n") + .toString(); } - public void setResponseMessage(String responseMessage) { - this.responseMessage = responseMessage; + public static Headers parse(String payload) { + String[] lines = payload.split("\n"); + + Headers headers = new Headers(); + for (String line: lines) { + line = line.trim(); + if (TextUtils.isEmpty(line)) + continue; + + headers.addLine(line); + } + return headers; } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java b/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java index bbcdf8fc6..b7178ee6c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/HttpUtil.java @@ -9,14 +9,13 @@ import com.koushikdutta.async.http.body.MultipartFormDataBody; import com.koushikdutta.async.http.body.StringBody; import com.koushikdutta.async.http.body.UrlEncodedFormBody; -import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.http.filter.ChunkedInputFilter; import com.koushikdutta.async.http.filter.ContentLengthFilter; import com.koushikdutta.async.http.filter.GZIPInputFilter; import com.koushikdutta.async.http.filter.InflaterInputFilter; public class HttpUtil { - public static AsyncHttpRequestBody getBody(DataEmitter emitter, CompletedCallback reporter, RawHeaders headers) { + public static AsyncHttpRequestBody getBody(DataEmitter emitter, CompletedCallback reporter, Headers headers) { String contentType = headers.get("Content-Type"); if (contentType != null) { String[] values = contentType.split(";"); @@ -60,7 +59,7 @@ public void run() { } } - public static DataEmitter getBodyDecoder(DataEmitter emitter, RawHeaders headers, boolean server) { + public static DataEmitter getBodyDecoder(DataEmitter emitter, Protocol protocol, Headers headers, boolean server) { long _contentLength; try { _contentLength = Long.parseLong(headers.get("Content-Length")); @@ -92,7 +91,7 @@ else if ("chunked".equalsIgnoreCase(headers.get("Transfer-Encoding"))) { emitter = chunker; } else { - if ((server || headers.getStatusLine().contains("HTTP/1.1")) && !"close".equalsIgnoreCase(headers.get("Connection"))) { + if ((server || protocol == Protocol.HTTP_1_1) && !"close".equalsIgnoreCase(headers.get("Connection"))) { // if this is the server, and the client has not indicated a request body, the client is done EndEmitter ender = EndEmitter.create(emitter.getServer(), null); ender.setDataEmitter(emitter); @@ -117,20 +116,23 @@ else if ("deflate".equals(headers.get("Content-Encoding"))) { return emitter; } - public static boolean isKeepAlive(RawHeaders headers) { - boolean keepAlive; + public static boolean isKeepAlive(Protocol protocol, Headers headers) { + // connection is always keep alive as this is an http/1.1 client String connection = headers.get("Connection"); - if (connection != null) { - keepAlive = "keep-alive".equalsIgnoreCase(connection); - } - else { - keepAlive = headers.getHttpMinorVersion() >= 1; - } + if (connection == null) + return protocol == Protocol.HTTP_1_1; + return "keep-alive".equalsIgnoreCase(connection); + } - return keepAlive; + public static boolean isKeepAlive(String protocol, Headers headers) { + // connection is always keep alive as this is an http/1.1 client + String connection = headers.get("Connection"); + if (connection == null) + return Protocol.get(protocol) == Protocol.HTTP_1_1; + return "keep-alive".equalsIgnoreCase(connection); } - public static int contentLength(RawHeaders headers) { + public static int contentLength(Headers headers) { String cl = headers.get("Content-Length"); if (cl == null) return -1; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java b/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java index 5223fc850..913532e50 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java @@ -2,14 +2,11 @@ import android.net.Uri; -import com.koushikdutta.async.http.cache.RawHeaders; - import org.apache.http.NameValuePair; import org.apache.http.message.BasicNameValuePair; import java.net.URLDecoder; import java.util.ArrayList; -import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -43,67 +40,69 @@ public void put(String name, String value) { put(name, ret); } - public Multimap(RawHeaders headers) { - headers.toMultimap().putAll(this); - } - public Multimap(List pairs) { for (NameValuePair pair: pairs) add(pair.getName(), pair.getValue()); } - public static Multimap parseHeader(String header) { - if (header == null) - return null; + public Multimap(Multimap m) { + putAll(m); + } + + public interface StringDecoder { + public String decode(String s); + } + + public static Multimap parse(String value, String delimiter, boolean unquote, StringDecoder decoder) { Multimap map = new Multimap(); - String[] parts = header.split(";"); + if (value == null) + return map; + String[] parts = value.split(delimiter); for (String part: parts) { String[] pair = part.split("=", 2); String key = pair[0].trim(); String v = null; if (pair.length > 1) v = pair[1]; - if (v != null && v.endsWith("\"") && v.startsWith("\"")) + if (unquote && v != null && v.endsWith("\"") && v.startsWith("\"")) v = v.substring(1, v.length() - 1); + if (decoder != null) { + key = decoder.decode(key); + v = decoder.decode(v); + } map.add(key, v); } return map; } - public static Multimap parseHeader(RawHeaders headers, String header) { - return parseHeader(headers.get(header)); + public static Multimap parseSemicolonDelimited(String header) { + return parse(header, ";", true, null); } - public static Multimap parseQuery(String query) { - Multimap map = new Multimap(); - String[] pairs = query.split("&"); - for (String p : pairs) { - String[] pair = p.split("=", 2); - if (pair.length == 0) - continue; - String name = Uri.decode(pair[0]); - String value = null; - if (pair.length == 2) - value = Uri.decode(pair[1]); - map.add(name, value); + public static Multimap parseCommaDelimited(String header) { + return parse(header, ",", true, null); + } + + private static final StringDecoder QUERY_DECODER = new StringDecoder() { + @Override + public String decode(String s) { + return Uri.decode(s); } - return map; + }; + + public static Multimap parseQuery(String query) { + return parse(query, "&", false, QUERY_DECODER); } - public static Multimap parseUrlEncoded(String query) { - Multimap map = new Multimap(); - String[] pairs = query.split("&"); - for (String p : pairs) { - String[] pair = p.split("=", 2); - if (pair.length == 0) - continue; - String name = URLDecoder.decode(pair[0]); - String value = null; - if (pair.length == 2) - value = URLDecoder.decode(pair[1]); - map.add(name, value); + private static final StringDecoder URL_DECODER = new StringDecoder() { + @Override + public String decode(String s) { + return URLDecoder.decode(s); } - return map; + }; + + public static Multimap parseUrlEncoded(String query) { + return parse(query, "&", false, URL_DECODER); } @Override diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Protocol.java b/AndroidAsync/src/com/koushikdutta/async/http/Protocol.java new file mode 100644 index 000000000..8e5a46c46 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/Protocol.java @@ -0,0 +1,89 @@ +package com.koushikdutta.async.http; + +import java.util.Hashtable; + +/** + * Protocols that OkHttp implements for NPN and + * ALPN + * selection. + *

+ *

Protocol vs Scheme

+ * Despite its name, {@link java.net.URL#getProtocol()} returns the + * {@linkplain java.net.URI#getScheme() scheme} (http, https, etc.) of the URL, not + * the protocol (http/1.1, spdy/3.1, etc.). OkHttp uses the word protocol + * to identify how HTTP messages are framed. + */ +public enum Protocol { + /** + * An obsolete plaintext framing that does not use persistent sockets by + * default. + */ + HTTP_1_0("http/1.0"), + + /** + * A plaintext framing that includes persistent connections. + *

+ *

This version of OkHttp implements RFC 2616, and tracks + * revisions to that spec. + */ + HTTP_1_1("http/1.1"), + + /** + * Chromium's binary-framed protocol that includes header compression, + * multiplexing multiple requests on the same socket, and server-push. + * HTTP/1.1 semantics are layered on SPDY/3. + *

+ *

This version of OkHttp implements SPDY 3 draft + * 3.1. Future releases of OkHttp may use this identifier for a newer draft + * of the SPDY spec. + */ + SPDY_3("spdy/3.1"), + + /** + * The IETF's binary-framed protocol that includes header compression, + * multiplexing multiple requests on the same socket, and server-push. + * HTTP/1.1 semantics are layered on HTTP/2. + *

+ *

This version of OkHttp implements HTTP/2 draft 12 + * with HPACK draft + * 6. Future releases of OkHttp may use this identifier for a newer draft + * of these specs. + */ + HTTP_2("h2-13"); + + private final String protocol; + private static final Hashtable protocols = new Hashtable(); + + static { + protocols.put(HTTP_1_0.toString(), HTTP_1_0); + protocols.put(HTTP_1_1.toString(), HTTP_1_1); + protocols.put(SPDY_3.toString(), SPDY_3); + protocols.put(HTTP_2.toString(), HTTP_2); + } + + + Protocol(String protocol) { + this.protocol = protocol; + } + + /** + * Returns the protocol identified by {@code protocol}. + */ + public static Protocol get(String protocol) { + return protocols.get(protocol.toLowerCase()); + } + + /** + * Returns the string used to identify this protocol for ALPN and NPN, like + * "http/1.1", "spdy/3.1" or "h2-13". + */ + @Override + public String toString() { + return protocol; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java index dba645a02..7fe545d86 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java @@ -3,14 +3,13 @@ import com.koushikdutta.async.future.Cancellable; public class SimpleMiddleware implements AsyncHttpClientMiddleware { - @Override - public Cancellable getSocket(GetSocketData data) { - return null; + public void onRequest(OnRequestData data) { } @Override - public void onSocket(OnSocketData data) { + public Cancellable getSocket(GetSocketData data) { + return null; } @Override diff --git a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java index 4af0b9961..ddbd62c66 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java @@ -11,7 +11,6 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; -import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; @@ -115,7 +114,7 @@ public WebSocketImpl(AsyncHttpServerRequest request, AsyncHttpServerResponse res String sha1 = SHA1(concat); String origin = request.getHeaders().get("Origin"); - response.responseCode(101); + response.code(101); response.getHeaders().set("Upgrade", "WebSocket"); response.getHeaders().set("Connection", "Upgrade"); response.getHeaders().set("Sec-WebSocket-Accept", sha1); @@ -131,7 +130,7 @@ public WebSocketImpl(AsyncHttpServerRequest request, AsyncHttpServerResponse res } public static void addWebSocketUpgradeHeaders(AsyncHttpRequest req, String protocol) { - RawHeaders headers = req.getHeaders(); + Headers headers = req.getHeaders(); final String key = Base64.encodeToString(toByteArray(UUID.randomUUID()),Base64.NO_WRAP); headers.set("Sec-WebSocket-Version", "13"); headers.set("Sec-WebSocket-Key", key); @@ -151,10 +150,10 @@ public WebSocketImpl(AsyncSocket socket) { mSink = new BufferedDataSink(mSocket); } - public static WebSocket finishHandshake(RawHeaders requestHeaders, AsyncHttpResponse response) { + public static WebSocket finishHandshake(Headers requestHeaders, AsyncHttpResponse response) { if (response == null) return null; - if (response.headers().getResponseCode() != 101) + if (response.code() != 101) return null; if (!"websocket".equalsIgnoreCase(response.headers().get("Upgrade"))) return null; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java b/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java index 1872e19c8..309d9c33e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java @@ -11,8 +11,8 @@ import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Continuation; import com.koushikdutta.async.http.AsyncHttpRequest; +import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Multimap; -import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.http.server.BoundaryEmitter; import java.io.File; @@ -21,7 +21,7 @@ public class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpRequestBody { LineEmitter liner; - RawHeaders formData; + Headers formData; ByteBufferList last; String lastName; @@ -40,7 +40,7 @@ void handleLast() { return; if (formData == null) - formData = new RawHeaders(); + formData = new Headers(); formData.add(lastName, last.peekString()); @@ -62,7 +62,7 @@ protected void onBoundaryEnd() { @Override protected void onBoundaryStart() { - final RawHeaders headers = new RawHeaders(); + final Headers headers = new Headers(); liner = new LineEmitter(); liner.setLineCallback(new StringCallback() { @Override @@ -146,8 +146,7 @@ public void onCompleted(Exception ex) { c.add(new ContinuationCallback() { @Override public void onContinue(Continuation continuation, CompletedCallback next) throws Exception { - part.getRawHeaders().setStatusLine(getBoundaryStart()); - byte[] bytes = part.getRawHeaders().toHeaderString().getBytes(); + byte[] bytes = part.getRawHeaders().toPrefixString(getBoundaryStart()).getBytes(); com.koushikdutta.async.Util.writeAll(sink, bytes, next); written += bytes.length; } @@ -205,10 +204,10 @@ public int length() { int length = 0; for (final Part part: mParts) { - part.getRawHeaders().setStatusLine(getBoundaryStart()); + String partHeader = part.getRawHeaders().toPrefixString(getBoundaryStart()); if (part.length() == -1) return -1; - length += part.length() + part.getRawHeaders().toHeaderString().getBytes().length + "\r\n".length(); + length += part.length() + partHeader.getBytes().length + "\r\n".length(); } length += (getBoundaryEnd()).getBytes().length; return totalToWrite = length; @@ -238,6 +237,6 @@ public void addPart(Part part) { @Override public Multimap get() { - return new Multimap(formData); + return new Multimap(formData.getMultiMap()); } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/body/Part.java b/AndroidAsync/src/com/koushikdutta/async/http/body/Part.java index 56fcecb58..b66c711e0 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/body/Part.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/body/Part.java @@ -2,8 +2,9 @@ import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; +import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Multimap; -import com.koushikdutta.async.http.cache.RawHeaders; + import org.apache.http.NameValuePair; import java.io.File; @@ -12,11 +13,11 @@ public class Part { public static final String CONTENT_DISPOSITION = "Content-Disposition"; - RawHeaders mHeaders; + Headers mHeaders; Multimap mContentDisposition; - public Part(RawHeaders headers) { + public Part(Headers headers) { mHeaders = headers; - mContentDisposition = Multimap.parseHeader(mHeaders, CONTENT_DISPOSITION); + mContentDisposition = Multimap.parseSemicolonDelimited(mHeaders.get(CONTENT_DISPOSITION)); } public String getName() { @@ -26,7 +27,7 @@ public String getName() { private long length = -1; public Part(String name, long length, List contentDisposition) { this.length = length; - mHeaders = new RawHeaders(); + mHeaders = new Headers(); StringBuilder builder = new StringBuilder(String.format("form-data; name=\"%s\"", name)); if (contentDisposition != null) { for (NameValuePair pair: contentDisposition) { @@ -34,10 +35,10 @@ public Part(String name, long length, List contentDisposition) { } } mHeaders.set(CONTENT_DISPOSITION, builder.toString()); - mContentDisposition = Multimap.parseHeader(mHeaders, CONTENT_DISPOSITION); + mContentDisposition = Multimap.parseSemicolonDelimited(mHeaders.get(CONTENT_DISPOSITION)); } - public RawHeaders getRawHeaders() { + public Headers getRawHeaders() { return mHeaders; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/cache/CacheUtil.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/CacheUtil.java deleted file mode 100644 index caadf8f30..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/cache/CacheUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.koushikdutta.async.http.cache; - -import java.util.HashSet; -import java.util.Set; - -/** - * Created by koush on 7/21/14. - */ -class CacheUtil { - static Set varyFields(RawHeaders headers) { - HashSet ret = new HashSet(); - String value = headers.get("Vary"); - if (value == null) - return ret; - for (String varyField : value.split(",")) { - ret.add(varyField.trim()); - } - return ret; - } - - static boolean isCacheable(RawHeaders requestHeaders, RawHeaders responseHeaders) { - ResponseHeaders r = new ResponseHeaders(null, responseHeaders); - return r.isCacheable(new RequestHeaders(null, requestHeaders)); - } - - static boolean isNoCache(RawHeaders headers) { - return new RequestHeaders(null, headers).isNoCache(); - } - - ResponseSource chooseResponseSource(long nowMillis, RawHeaders request, RawHeaders response) { - RequestHeaders requestHeaders = new RequestHeaders(null, request); - ResponseHeaders responseHeaders = new ResponseHeaders(null, response); - return responseHeaders.chooseResponseSource(nowMillis, requestHeaders); - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/cache/RawHeaders.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/RawHeaders.java index 53be34091..9274a54e3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/cache/RawHeaders.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/RawHeaders.java @@ -44,7 +44,7 @@ *

This class trims whitespace from values. It never returns values with * leading or trailing whitespace. */ -public final class RawHeaders { +final class RawHeaders { private static final Comparator FIELD_NAME_COMPARATOR = new Comparator() { @Override public int compare(String a, String b) { if (a == b) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java index 22f0ac2a3..447c81b05 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java @@ -18,6 +18,7 @@ import com.koushikdutta.async.http.AsyncHttpClientMiddleware; import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.AsyncHttpRequest; +import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.SimpleMiddleware; import com.koushikdutta.async.util.Allocator; import com.koushikdutta.async.util.Charsets; @@ -95,7 +96,10 @@ public void setCaching(boolean caching) { // also see if this can be turned into a conditional cache request. @Override public Cancellable getSocket(final GetSocketData data) { - if (cache == null || !caching || CacheUtil.isNoCache(data.request.getHeaders())) { + RequestHeaders requestHeaders = new RequestHeaders(data.request.getUri(), RawHeaders.fromMultimap(data.request.getHeaders().getMultiMap())); + data.state.put("request-headers", requestHeaders); + + if (cache == null || !caching || requestHeaders.isNoCache()) { networkCount++; return null; } @@ -121,7 +125,7 @@ public Cancellable getSocket(final GetSocketData data) { } // verify the entry matches - if (!entry.matches(data.request.getUri(), data.request.getMethod(), data.request.getHeaders().toMultimap())) { + if (!entry.matches(data.request.getUri(), data.request.getMethod(), data.request.getHeaders().getMultiMap())) { networkCount++; StreamUtility.closeQuietly(snapshot); return null; @@ -154,7 +158,6 @@ public Cancellable getSocket(final GetSocketData data) { cachedResponseHeaders.setLocalTimestamps(System.currentTimeMillis(), System.currentTimeMillis()); long now = System.currentTimeMillis(); - RequestHeaders requestHeaders = new RequestHeaders(null, data.request.getHeaders()); ResponseSource responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders); if (responseSource == ResponseSource.CACHE) { @@ -218,13 +221,18 @@ public void onBodyDecoder(OnBodyData data) { } CacheData cacheData = data.state.get("cache-data"); + RawHeaders rh = RawHeaders.fromMultimap(data.headers.getMultiMap()); + rh.setStatusLine(String.format("%s %s %s", data.response.protocol(), data.response.code(), data.response.message())); + ResponseHeaders networkResponse = new ResponseHeaders(data.request.getUri(), rh); + data.state.put("response-headers", networkResponse); if (cacheData != null) { - ResponseHeaders networkResponse = new ResponseHeaders(null, data.headers); if (cacheData.cachedResponseHeaders.validate(networkResponse)) { data.request.logi("Serving response from conditional cache"); data.headers.removeAll("Content-Length"); - data.headers = cacheData.cachedResponseHeaders.combine(networkResponse).getHeaders(); - data.headers.setStatusLine(cacheData.cachedResponseHeaders.getHeaders().getStatusLine()); + ResponseHeaders combined = cacheData.cachedResponseHeaders.combine(networkResponse); + data.headers = new Headers(combined.getHeaders().toMultimap()); + data.response.code(combined.getHeaders().getResponseCode()); + data.response.message(combined.getHeaders().getResponseMessage()); data.headers.set(SERVED_FROM, CONDITIONAL_CACHE); conditionalCacheHitCount++; @@ -244,7 +252,8 @@ public void onBodyDecoder(OnBodyData data) { if (!caching) return; - if (!CacheUtil.isCacheable(data.request.getHeaders(), data.headers) || !data.request.getMethod().equals(AsyncHttpGet.METHOD)) { + RequestHeaders requestHeaders = data.state.get("request-headers"); + if (requestHeaders == null || !networkResponse.isCacheable(requestHeaders) || !data.request.getMethod().equals(AsyncHttpGet.METHOD)) { /* * Don't cache non-GET responses. We're technically allowed to cache * HEAD requests and some POST requests, but the complexity of doing @@ -256,8 +265,8 @@ public void onBodyDecoder(OnBodyData data) { } String key = FileCache.toKeyString(data.request.getUri()); - RawHeaders varyHeaders = data.request.getHeaders().getAll(CacheUtil.varyFields(data.headers)); - Entry entry = new Entry(data.request.getUri(), varyHeaders, data.request, data.headers); + RawHeaders varyHeaders = requestHeaders.getHeaders().getAll(networkResponse.getVaryFields()); + Entry entry = new Entry(data.request.getUri(), varyHeaders, data.request, networkResponse.getHeaders()); BodyCacher cacher = new BodyCacher(); EntryEditor editor = new EntryEditor(key); try { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java b/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java deleted file mode 100644 index 68db69f2a..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/callback/HeadersCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.koushikdutta.async.http.callback; - -import com.koushikdutta.async.http.cache.RawHeaders; - -/** - * Created by koush on 6/30/13. - */ -public interface HeadersCallback { - public void onHeaders(RawHeaders headers); -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/ContentLengthFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/ContentLengthFilter.java index d1b05a2ef..41aa217b3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/ContentLengthFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/ContentLengthFilter.java @@ -12,7 +12,7 @@ public ContentLengthFilter(long contentLength) { @Override protected void report(Exception e) { if (e == null && totalRead != contentLength) - e = new PrematureDataEndException("End of data reached before content length was read"); + e = new PrematureDataEndException("End of data reached before content length was read: " + totalRead + "/" + contentLength + " Paused: " + isPaused()); super.report(e); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java index 1c981b796..3d6566933 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java @@ -20,16 +20,18 @@ import com.koushikdutta.async.http.AsyncHttpGet; import com.koushikdutta.async.http.AsyncHttpHead; import com.koushikdutta.async.http.AsyncHttpPost; +import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.HttpUtil; import com.koushikdutta.async.http.Multimap; +import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.WebSocket; import com.koushikdutta.async.http.WebSocketImpl; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; -import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.util.StreamUtility; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -52,10 +54,16 @@ public void stop() { } } - protected void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { + protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { + return false; } - protected AsyncHttpRequestBody onUnknownBody(RawHeaders headers) { + protected void onRequest(HttpServerRequestCallback callback, AsyncHttpServerRequest request, AsyncHttpServerResponse response) { + if (callback != null) + callback.onRequest(request, response); + } + + protected AsyncHttpRequestBody onUnknownBody(Headers headers) { return new UnknownRequestBody(headers.get("Content-Type")); } @@ -63,7 +71,7 @@ protected AsyncHttpRequestBody onUnknownBody(RawHeaders headers) { @Override public void onAccepted(final AsyncSocket socket) { AsyncHttpServerRequestImpl req = new AsyncHttpServerRequestImpl() { - Pair match; + HttpServerRequestCallback match; String fullPath; String path; boolean responseComplete; @@ -72,13 +80,13 @@ public void onAccepted(final AsyncSocket socket) { boolean hasContinued; @Override - protected AsyncHttpRequestBody onUnknownBody(RawHeaders headers) { + protected AsyncHttpRequestBody onUnknownBody(Headers headers) { return AsyncHttpServer.this.onUnknownBody(headers); } @Override protected void onHeadersReceived() { - RawHeaders headers = getHeaders(); + Headers headers = getHeaders(); // should the negotiation of 100 continue be here, or in the request impl? // probably here, so AsyncResponse can negotiate a 100 continue. @@ -101,7 +109,7 @@ public void onCompleted(Exception ex) { } // System.out.println(headers.toHeaderString()); - String statusLine = headers.getStatusLine(); + String statusLine = getStatusLine(); String[] parts = statusLine.split(" "); fullPath = parts[1]; path = fullPath.split("\\?")[0]; @@ -113,7 +121,7 @@ public void onCompleted(Exception ex) { Matcher m = p.regex.matcher(path); if (m.matches()) { mMatcher = m; - match = p; + match = p.callback; break; } } @@ -130,26 +138,26 @@ protected void onEnd() { } }; - onRequest(this, res); - - if (match == null) { - res.responseCode(404); + boolean handled = onRequest(this, res); + + if (match == null && !handled) { + res.code(404); res.end(); return; } if (!getBody().readFullyOnRequest()) { - match.callback.onRequest(this, res); + onRequest(match, this, res); } else if (requestComplete) { - match.callback.onRequest(this, res); + onRequest(match, this, res); } } @Override public void onCompleted(Exception e) { // if the protocol was switched off http, ignore this request/response. - if (res.getHeaders().getResponseCode() == 101) + if (res.code() == 101) return; requestComplete = true; super.onCompleted(e); @@ -165,14 +173,13 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { handleOnCompleted(); if (getBody().readFullyOnRequest()) { - if (match != null) - match.callback.onRequest(this, res); + onRequest(match, this, res); } } private void handleOnCompleted() { if (requestComplete && responseComplete) { - if (HttpUtil.isKeepAlive(getHeaders())) { + if (HttpUtil.isKeepAlive(Protocol.HTTP_1_1, getHeaders())) { onAccepted(socket); } else { @@ -266,7 +273,7 @@ private static class Pair { HttpServerRequestCallback callback; } - Hashtable> mActions = new Hashtable>(); + final Hashtable> mActions = new Hashtable>(); public void addAction(String action, String regex, HttpServerRequestCallback callback) { Pair p = new Pair(); @@ -284,7 +291,7 @@ public void addAction(String action, String regex, HttpServerRequestCallback cal } public static interface WebSocketRequestCallback { - public void onConnected(WebSocket webSocket, RawHeaders headers); + public void onConnected(WebSocket webSocket, Headers headers); } public void websocket(String regex, final WebSocketRequestCallback callback) { @@ -307,13 +314,13 @@ public void onRequest(final AsyncHttpServerRequest request, final AsyncHttpServe } } if (!"websocket".equalsIgnoreCase(request.getHeaders().get("Upgrade")) || !hasUpgrade) { - response.responseCode(404); + response.code(404); response.end(); return; } String peerProtocol = request.getHeaders().get("Sec-WebSocket-Protocol"); if (!TextUtils.equals(protocol, peerProtocol)) { - response.responseCode(404); + response.code(404); response.end(); return; } @@ -379,14 +386,14 @@ public void directory(Context context, String regex, final String assetPath) { public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { String path = request.getMatcher().replaceAll(""); android.util.Pair pair = getAssetStream(_context, assetPath + path); - final InputStream is = pair.second; - response.getHeaders().set("Content-Length", String.valueOf(pair.first)); - if (is == null) { - response.responseCode(404); + if (pair == null || pair.second == null) { + response.code(404); response.end(); return; } - response.responseCode(200); + final InputStream is = pair.second; + response.getHeaders().set("Content-Length", String.valueOf(pair.first)); + response.code(200); response.getHeaders().add("Content-Type", getContentType(assetPath + path)); Util.pump(is, response, new CompletedCallback() { @Override @@ -402,15 +409,15 @@ public void onCompleted(Exception ex) { public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { String path = request.getMatcher().replaceAll(""); android.util.Pair pair = getAssetStream(_context, assetPath + path); - final InputStream is = pair.second; - StreamUtility.closeQuietly(is); - response.getHeaders().set("Content-Length", String.valueOf(pair.first)); - if (is == null) { - response.responseCode(404); + if (pair == null || pair.second == null) { + response.code(404); response.end(); return; } - response.responseCode(200); + final InputStream is = pair.second; + StreamUtility.closeQuietly(is); + response.getHeaders().set("Content-Length", String.valueOf(pair.first)); + response.code(200); response.getHeaders().add("Content-Type", getContentType(assetPath + path)); response.writeHead(); response.end(); @@ -455,13 +462,13 @@ public int compare(File lhs, File rhs) { return; } if (!file.isFile()) { - response.responseCode(404); + response.code(404); response.end(); return; } try { FileInputStream is = new FileInputStream(file); - response.responseCode(200); + response.code(200); Util.pump(is, response, new CompletedCallback() { @Override public void onCompleted(Exception ex) { @@ -469,10 +476,9 @@ public void onCompleted(Exception ex) { } }); } - catch (Exception ex) { - response.responseCode(404); + catch (FileNotFoundException ex) { + response.code(404); response.end(); - return; } } }); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java index 77ce87545..1b0292f39 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequest.java @@ -2,14 +2,14 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; -import com.koushikdutta.async.http.cache.RawHeaders; import java.util.regex.Matcher; public interface AsyncHttpServerRequest extends DataEmitter { - public RawHeaders getHeaders(); + public Headers getHeaders(); public Matcher getMatcher(); public AsyncHttpRequestBody getBody(); public AsyncSocket getSocket(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java index ba0576fd3..15bed1025 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java @@ -7,17 +7,23 @@ import com.koushikdutta.async.LineEmitter.StringCallback; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; +import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.HttpUtil; +import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; -import com.koushikdutta.async.http.cache.RawHeaders; import java.util.regex.Matcher; public abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter implements AsyncHttpServerRequest, CompletedCallback { - private RawHeaders mRawHeaders = new RawHeaders(); + private String statusLine; + private Headers mRawHeaders = new Headers(); AsyncSocket mSocket; Matcher mMatcher; + public String getStatusLine() { + return statusLine; + } + private CompletedCallback mReporter = new CompletedCallback() { @Override public void onCompleted(Exception error) { @@ -35,11 +41,10 @@ public void onCompleted(Exception e) { abstract protected void onHeadersReceived(); protected void onNotHttp() { - System.out.println("not http: " + mRawHeaders.getStatusLine()); - System.out.println("not http: " + mRawHeaders.getStatusLine().length()); + System.out.println("not http!"); } - protected AsyncHttpRequestBody onUnknownBody(RawHeaders headers) { + protected AsyncHttpRequestBody onUnknownBody(Headers headers) { return null; } @@ -47,9 +52,9 @@ protected AsyncHttpRequestBody onUnknownBody(RawHeaders headers) { @Override public void onStringAvailable(String s) { try { - if (mRawHeaders.getStatusLine() == null) { - mRawHeaders.setStatusLine(s); - if (!mRawHeaders.getStatusLine().contains("HTTP/")) { + if (statusLine == null) { + statusLine = s; + if (!statusLine.contains("HTTP/")) { onNotHttp(); mSocket.setDataCallback(null); } @@ -58,7 +63,7 @@ else if (!"\r".equals(s)){ mRawHeaders.addLine(s); } else { - DataEmitter emitter = HttpUtil.getBodyDecoder(mSocket, mRawHeaders, true); + DataEmitter emitter = HttpUtil.getBodyDecoder(mSocket, Protocol.HTTP_1_1, mRawHeaders, true); // emitter.setEndCallback(mReporter); mBody = HttpUtil.getBody(emitter, mReporter, mRawHeaders); if (mBody == null) { @@ -96,7 +101,7 @@ public AsyncSocket getSocket() { } @Override - public RawHeaders getHeaders() { + public Headers getHeaders() { return mRawHeaders; } @@ -140,4 +145,11 @@ public void resume() { public boolean isPaused() { return mSocket.isPaused(); } + + @Override + public String toString() { + if (mRawHeaders == null) + return super.toString(); + return mRawHeaders.toPrefixString(statusLine); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java index 87fbc5e2e..bc6e33295 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java @@ -3,7 +3,7 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; -import com.koushikdutta.async.http.cache.RawHeaders; +import com.koushikdutta.async.http.Headers; import org.json.JSONObject; @@ -17,8 +17,9 @@ public interface AsyncHttpServerResponse extends DataSink, CompletedCallback { public void send(JSONObject json); public void sendFile(File file); public void sendStream(InputStream inputStream, long totalLength); - public void responseCode(int code); - public RawHeaders getHeaders(); + public AsyncHttpServerResponse code(int code); + public int code(); + public Headers getHeaders(); public void writeHead(); public void setContentType(String contentType); public void redirect(String location); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java index 0fe4e1001..3cc5d63f6 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java @@ -11,8 +11,10 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.AsyncHttpHead; +import com.koushikdutta.async.http.AsyncHttpResponse; +import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.HttpUtil; -import com.koushikdutta.async.http.cache.RawHeaders; +import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.filter.ChunkedOutputFilter; import com.koushikdutta.async.util.StreamUtility; @@ -21,15 +23,16 @@ import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.InputStream; import java.io.UnsupportedEncodingException; public class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { - private RawHeaders mRawHeaders = new RawHeaders(); + private Headers mRawHeaders = new Headers(); private long mContentLength = -1; @Override - public RawHeaders getHeaders() { + public Headers getHeaders() { return mRawHeaders; } @@ -42,7 +45,7 @@ public AsyncSocket getSocket() { AsyncHttpServerResponseImpl(AsyncSocket socket, AsyncHttpServerRequestImpl req) { mSocket = socket; mRequest = req; - if (HttpUtil.isKeepAlive(req.getHeaders())) + if (HttpUtil.isKeepAlive(Protocol.HTTP_1_1, req.getHeaders())) mRawHeaders.set("Connection", "Keep-Alive"); } @@ -69,7 +72,6 @@ void initFirstWrite() { return; mHasWritten = true; - assert null != mRawHeaders.getStatusLine(); String currentEncoding = mRawHeaders.get("Transfer-Encoding"); if ("".equals(currentEncoding)) mRawHeaders.removeAll("Transfer-Encoding"); @@ -129,7 +131,9 @@ public void writeHead() { private void writeHeadInternal() { assert !mHeadWritten; mHeadWritten = true; - Util.writeAll(mSocket, mRawHeaders.toHeaderString().getBytes(), new CompletedCallback() { + String statusLine = String.format("HTTP/1.1 %s %s", code, AsyncHttpServer.getResponseCodeDescription(code)); + String rh = mRawHeaders.toPrefixString(statusLine); + Util.writeAll(mSocket, rh.getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { // TODO: HACK!!! @@ -153,8 +157,6 @@ public void setContentType(String contentType) { @Override public void send(String contentType, final String string) { try { - if (mRawHeaders.getStatusLine() == null) - responseCode(200); assert mContentLength < 0; byte[] bytes = string.getBytes("UTF-8"); mContentLength = bytes.length; @@ -184,7 +186,6 @@ protected void report(Exception e) { @Override public void send(String string) { - responseCode(200); String contentType = mRawHeaders.get("Content-Type"); if (contentType == null) contentType = "text/html; charset=utf8"; @@ -206,7 +207,7 @@ public void sendStream(final InputStream inputStream, long totalLength) { String[] parts = range.split("="); if (parts.length != 2 || !"bytes".equals(parts[0])) { // Requested range not satisfiable - responseCode(416); + code(416); end(); return; } @@ -222,11 +223,11 @@ public void sendStream(final InputStream inputStream, long totalLength) { else end = totalLength - 1; - responseCode(206); + code(206); getHeaders().set("Content-Range", String.format("bytes %d-%d/%d", start, end, totalLength)); } catch (Exception e) { - responseCode(416); + code(416); end(); return; } @@ -237,8 +238,6 @@ public void sendStream(final InputStream inputStream, long totalLength) { mContentLength = end - start + 1; mRawHeaders.set("Content-Length", String.valueOf(mContentLength)); mRawHeaders.set("Accept-Ranges", "bytes"); - if (getHeaders().getStatusLine() == null) - responseCode(200); if (mRequest.getMethod().equals(AsyncHttpHead.METHOD)) { writeHead(); onEnd(); @@ -253,7 +252,7 @@ public void onCompleted(Exception ex) { }); } catch (Exception e) { - responseCode(404); + code(500); end(); } } @@ -266,21 +265,27 @@ public void sendFile(File file) { FileInputStream fin = new FileInputStream(file); sendStream(new BufferedInputStream(fin, 64000), file.length()); } - catch (Exception e) { - responseCode(404); + catch (FileNotFoundException e) { + code(404); end(); } } + int code = 200; + @Override + public AsyncHttpServerResponse code(int code) { + this.code = code; + return this; + } + @Override - public void responseCode(int code) { - String status = AsyncHttpServer.getResponseCodeDescription(code); - mRawHeaders.setStatusLine(String.format("HTTP/1.1 %d %s", code, status)); + public int code() { + return code; } @Override public void redirect(String location) { - responseCode(302); + code(302); mRawHeaders.set("Location", location); end(); } @@ -311,4 +316,12 @@ public CompletedCallback getClosedCallback() { public AsyncServer getServer() { return mSocket.getServer(); } + + @Override + public String toString() { + if (mRawHeaders == null) + return super.toString(); + String statusLine = String.format("HTTP/1.1 %s %s", code, AsyncHttpServer.getResponseCodeDescription(code)); + return mRawHeaders.toPrefixString(statusLine); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncProxyServer.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncProxyServer.java new file mode 100644 index 000000000..8a16c092a --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncProxyServer.java @@ -0,0 +1,81 @@ +package com.koushikdutta.async.http.server; + +import android.net.Uri; + +import com.koushikdutta.async.AsyncServer; +import com.koushikdutta.async.Util; +import com.koushikdutta.async.callback.CompletedCallback; +import com.koushikdutta.async.http.AsyncHttpClient; +import com.koushikdutta.async.http.AsyncHttpRequest; +import com.koushikdutta.async.http.AsyncHttpResponse; +import com.koushikdutta.async.http.callback.HttpConnectCallback; + +/** + * Created by koush on 7/22/14. + */ +public class AsyncProxyServer extends AsyncHttpServer { + AsyncHttpClient proxyClient; + public AsyncProxyServer(AsyncServer server) { + proxyClient = new AsyncHttpClient(server); + } + + @Override + protected void onRequest(HttpServerRequestCallback callback, AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { + super.onRequest(callback, request, response); + + if (callback != null) + return; + + try { + Uri uri; + + try { + uri = Uri.parse(request.getPath()); + if (uri.getScheme() == null) + throw new Exception("no host or full uri provided"); + } + catch (Exception e) { + String host = request.getHeaders().get("Host"); + int port = 80; + if (host != null) { + String[] splits = host.split(":", 2); + if (splits.length == 2) { + host = splits[0]; + port = Integer.parseInt(splits[1]); + } + } + uri = Uri.parse("http://" + host + ":" + port + request.getPath()); + } + + proxyClient.execute(new AsyncHttpRequest(uri, request.getMethod(), request.getHeaders()), new HttpConnectCallback() { + @Override + public void onConnectCompleted(Exception ex, AsyncHttpResponse remoteResponse) { + if (ex != null) { + response.code(500); + response.send(ex.getMessage()); + return; + } + response.code(remoteResponse.code()); + response.getHeaders().addAll(remoteResponse.headers()); + response.getHeaders().removeAll("Transfer-Encoding"); + response.getHeaders().removeAll("Content-Encoding"); + Util.pump(remoteResponse, response, new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + response.end(); + } + }); + } + }); + } + catch (Exception e) { + response.code(500); + response.send(e.getMessage()); + } + } + + @Override + protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { + return true; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index 484b6d4e7..18e856cf1 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -1,7 +1,5 @@ package com.koushikdutta.async.http.spdy; -import android.text.TextUtils; - import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.BufferedDataEmitter; @@ -11,8 +9,7 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; -import com.koushikdutta.async.http.spdy.okhttp.Protocol; -import com.koushikdutta.async.http.spdy.okhttp.internal.NamedRunnable; +import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.ErrorCode; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameReader; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameWriter; @@ -22,14 +19,11 @@ import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Ping; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.SpdyConnection; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.SpdyStream; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Variant; import com.koushikdutta.async.http.spdy.okio.BufferedSource; import com.koushikdutta.async.http.spdy.okio.ByteString; import java.io.IOException; -import java.nio.ByteBuffer; import java.util.Hashtable; import java.util.Iterator; import java.util.List; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index 837a12666..35d425782 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -8,7 +8,7 @@ import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncSSLEngineConfigurator; import com.koushikdutta.async.http.AsyncSSLSocketMiddleware; -import com.koushikdutta.async.http.spdy.okhttp.Protocol; +import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.util.Charsets; import java.lang.reflect.Field; diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java index b9645e407..5be77f90e 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java @@ -26,6 +26,7 @@ import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; +import com.koushikdutta.async.http.server.AsyncProxyServer; import com.koushikdutta.async.http.server.HttpServerRequestCallback; import junit.framework.Assert; @@ -281,23 +282,13 @@ public void testProxy() throws Exception { wasProxied = false; final AsyncServer proxyServer = new AsyncServer(); try { - AsyncHttpServer httpServer = new AsyncHttpServer(); - httpServer.get(".*", new HttpServerRequestCallback() { + AsyncProxyServer httpServer = new AsyncProxyServer(proxyServer) { @Override - public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { - Log.i("Proxy", "Proxying request"); + protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { wasProxied = true; - AsyncHttpClient proxying = new AsyncHttpClient(proxyServer); - - String url = request.getPath(); - proxying.executeString(new AsyncHttpGet(url), new StringCallback() { - @Override - public void onCompleted(Exception e, AsyncHttpResponse source, String result) { - response.send(result); - } - }); + return super.onRequest(request, response); } - }); + }; AsyncServerSocket socket = httpServer.listen(proxyServer, 0); diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java b/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java index a72f306c3..18aa00b45 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/Issue59.java @@ -29,7 +29,7 @@ public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerRespo // setting this to empty is a hacky way of telling the framework not to use // transfer-encoding. It will get removed. response.getHeaders().set("Transfer-Encoding", ""); - response.responseCode(200); + response.code(200); Util.writeAll(response, "foobarbeepboop".getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java b/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java index d63494b63..821de08fd 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java @@ -5,7 +5,7 @@ import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.http.spdy.okhttp.Handshake; -import com.koushikdutta.async.http.spdy.okhttp.Protocol; +import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.util.Charsets; import org.conscrypt.OpenSSLProvider; diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/WebSocketTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/WebSocketTests.java index dcbe6c810..03de7da91 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/WebSocketTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/WebSocketTests.java @@ -4,9 +4,9 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.AsyncHttpClient.WebSocketConnectCallback; +import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.WebSocket; import com.koushikdutta.async.http.WebSocket.StringCallback; -import com.koushikdutta.async.http.cache.RawHeaders; import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServer.WebSocketRequestCallback; @@ -34,7 +34,7 @@ public void onCompleted(Exception ex) { httpServer.websocket("/ws", new WebSocketRequestCallback() { @Override - public void onConnected(final WebSocket webSocket, RawHeaders headers) { + public void onConnected(final WebSocket webSocket, Headers headers) { webSocket.setStringCallback(new StringCallback() { @Override public void onStringAvailable(String s) { From e65860783f5aaf389ba2cab560bdef9a030dc077 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 23 Jul 2014 01:22:08 -0700 Subject: [PATCH 037/399] Behavior fixes around BufferedDataEmitter. --- .../async/AsyncSSLSocketWrapper.java | 22 +++++++++--- .../async/BufferedDataEmitter.java | 34 +++++++++---------- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 84d7da1c1..44981128e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -141,8 +141,21 @@ public void onWriteable() { } }); - // SSL needs buffering of data written during handshake. - // aka exhcange.setDatacallback + + // here's the stack of emitters + // ssl emitter + // buffered data emitter + // socket + + // ssl emitter needs a buffered emitter + // in case there is an underflow. + // buffered emitter will read from the socket, + // and replay data forever. + + // on pause, the emitter is paused to prevent the buffered + // socket and itself from firing. + // on resume, emitter is resumed, ssl buffer is flushed as well + mEmitter = new BufferedDataEmitter(socket); mEmitter.setEndCallback(new CompletedCallback() { @Override @@ -467,14 +480,13 @@ public CompletedCallback getEndCallback() { @Override public void pause() { - mSocket.pause(); + mEmitter.pause(); } @Override public void resume() { + mEmitter.resume(); onDataAvailable(); - mEmitter.onDataAvailable(); - mSocket.resume(); } @Override diff --git a/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java b/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java index 009f6c6bd..4ddc3f225 100644 --- a/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java +++ b/AndroidAsync/src/com/koushikdutta/async/BufferedDataEmitter.java @@ -3,12 +3,18 @@ import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; -public class BufferedDataEmitter implements DataEmitter, DataCallback { +public class BufferedDataEmitter implements DataEmitter { DataEmitter mEmitter; public BufferedDataEmitter(DataEmitter emitter) { mEmitter = emitter; - mEmitter.setDataCallback(this); - + mEmitter.setDataCallback(new DataCallback() { + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + bb.get(mBuffers); + BufferedDataEmitter.this.onDataAvailable(); + } + }); + mEmitter.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { @@ -29,9 +35,9 @@ public void close() { Exception mEndException; public void onDataAvailable() { - if (mDataCallback != null && !mPaused && mBuffers.remaining() > 0) + if (mDataCallback != null && !isPaused() && mBuffers.remaining() > 0) mDataCallback.onDataAvailable(this, mBuffers); - + if (mEnded && mBuffers.remaining() == 0) mEndCallback.onCompleted(mEndException); } @@ -41,6 +47,8 @@ public void onDataAvailable() { DataCallback mDataCallback; @Override public void setDataCallback(DataCallback callback) { + if (mDataCallback != null) + throw new RuntimeException("Buffered Data Emitter callback may only be set once"); mDataCallback = callback; } @@ -54,30 +62,20 @@ public boolean isChunked() { return false; } - @Override - public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { - bb.get(mBuffers); - - onDataAvailable(); - } - - private boolean mPaused; @Override public void pause() { - mPaused = true; + mEmitter.pause(); } @Override public void resume() { - if (!mPaused) - return; - mPaused = false; + mEmitter.resume(); onDataAvailable(); } @Override public boolean isPaused() { - return mPaused; + return mEmitter.isPaused(); } From 9ea60965820b604fb24373162300eb640a9da15a Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 23 Jul 2014 19:16:52 -0700 Subject: [PATCH 038/399] Remove usage of BufferedDataEmitter. Buffering done in SSL Socket. --- .../async/AsyncSSLSocketWrapper.java | 142 +++++++++--------- 1 file changed, 72 insertions(+), 70 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 44981128e..54138960d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -35,7 +35,6 @@ public interface HandshakeCallback { static SSLContext defaultSSLContext; AsyncSocket mSocket; - BufferedDataEmitter mEmitter; BufferedDataSink mSink; boolean mUnwrapping; SSLEngine engine; @@ -117,7 +116,7 @@ public void onCompleted(Exception ex) { boolean mEnded; Exception mEndException; - final ByteBufferList transformed = new ByteBufferList(); + final ByteBufferList pending = new ByteBufferList(); private AsyncSSLSocketWrapper(AsyncSocket socket, String host, int port, @@ -155,90 +154,93 @@ public void onWriteable() { // on pause, the emitter is paused to prevent the buffered // socket and itself from firing. // on resume, emitter is resumed, ssl buffer is flushed as well - - mEmitter = new BufferedDataEmitter(socket); - mEmitter.setEndCallback(new CompletedCallback() { + mSocket.setEndCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { if (mEnded) return; mEnded = true; mEndException = ex; - if (!transformed.hasRemaining() && mEndCallback != null) + if (!pending.hasRemaining() && mEndCallback != null) mEndCallback.onCompleted(ex); } }); - final Allocator allocator = new Allocator(); - allocator.setMinAlloc(8192); - mEmitter.setDataCallback(new DataCallback() { - @Override - public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { - if (mUnwrapping) - return; - try { - mUnwrapping = true; + mSocket.setDataCallback(dataCallback); + } - if (bb.hasRemaining()) { - ByteBuffer all = bb.getAll(); - bb.add(all); - } + final DataCallback dataCallback = new DataCallback() { + final Allocator allocator = new Allocator().setMinAlloc(8192); + final ByteBufferList buffered = new ByteBufferList(); - ByteBuffer b = ByteBufferList.EMPTY_BYTEBUFFER; - while (true) { - if (b.remaining() == 0 && bb.size() > 0) { - b = bb.remove(); - } - int remaining = b.remaining(); - int before = transformed.remaining(); - - SSLEngineResult res; - { - // wrap to prevent access to the readBuf - ByteBuffer readBuf = allocator.allocate(); - res = engine.unwrap(b, readBuf); - addToPending(transformed, readBuf); - allocator.track(transformed.remaining() - before); - } - if (res.getStatus() == Status.BUFFER_OVERFLOW) { - allocator.setMinAlloc(allocator.getMinAlloc() * 2); - remaining = -1; - } - else if (res.getStatus() == Status.BUFFER_UNDERFLOW) { - bb.addFirst(b); - if (bb.size() <= 1) { - break; - } - // pack it - remaining = -1; - b = bb.getAll(); - bb.addFirst(b); - b = ByteBufferList.EMPTY_BYTEBUFFER; - } - handleHandshakeStatus(res.getHandshakeStatus()); - if (b.remaining() == remaining && before == transformed.remaining()) { - bb.addFirst(b); + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + if (mUnwrapping) + return; + try { + mUnwrapping = true; + + bb.get(buffered); + + if (buffered.hasRemaining()) { + ByteBuffer all = buffered.getAll(); + buffered.add(all); + } + + ByteBuffer b = ByteBufferList.EMPTY_BYTEBUFFER; + while (true) { + if (b.remaining() == 0 && buffered.size() > 0) { + b = buffered.remove(); + } + int remaining = b.remaining(); + int before = pending.remaining(); + + SSLEngineResult res; + { + // wrap to prevent access to the readBuf + ByteBuffer readBuf = allocator.allocate(); + res = engine.unwrap(b, readBuf); + addToPending(pending, readBuf); + allocator.track(pending.remaining() - before); + } + if (res.getStatus() == Status.BUFFER_OVERFLOW) { + allocator.setMinAlloc(allocator.getMinAlloc() * 2); + remaining = -1; + } + else if (res.getStatus() == Status.BUFFER_UNDERFLOW) { + buffered.addFirst(b); + if (buffered.size() <= 1) { break; } + // pack it + remaining = -1; + b = buffered.getAll(); + buffered.addFirst(b); + b = ByteBufferList.EMPTY_BYTEBUFFER; + } + handleHandshakeStatus(res.getHandshakeStatus()); + if (b.remaining() == remaining && before == pending.remaining()) { + buffered.addFirst(b); + break; } - - AsyncSSLSocketWrapper.this.onDataAvailable(); - } - catch (SSLException ex) { - ex.printStackTrace(); - report(ex); - } - finally { - mUnwrapping = false; } + + AsyncSSLSocketWrapper.this.onDataAvailable(); } - }); - } + catch (SSLException ex) { + ex.printStackTrace(); + report(ex); + } + finally { + mUnwrapping = false; + } + } + }; public void onDataAvailable() { - Util.emitAllData(this, transformed); + Util.emitAllData(this, pending); - if (mEnded && !transformed.hasRemaining()) + if (mEnded && !pending.hasRemaining()) mEndCallback.onCompleted(mEndException); } @@ -283,7 +285,7 @@ private void handleHandshakeStatus(HandshakeStatus status) { } if (status == HandshakeStatus.NEED_UNWRAP) { - mEmitter.onDataAvailable(); + dataCallback.onDataAvailable(this, new ByteBufferList()); } try { @@ -336,7 +338,7 @@ private void handleHandshakeStatus(HandshakeStatus status) { handshakeCallback = null; if (mWriteableCallback != null) mWriteableCallback.onWriteable(); - mEmitter.onDataAvailable(); + onDataAvailable(); } } catch (NoSuchAlgorithmException ex) { @@ -480,12 +482,12 @@ public CompletedCallback getEndCallback() { @Override public void pause() { - mEmitter.pause(); + mSocket.pause(); } @Override public void resume() { - mEmitter.resume(); + mSocket.resume(); onDataAvailable(); } From 26c3c5bf969faadaf3d001b58c4c2731b9b0154a Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 23 Jul 2014 22:29:38 -0700 Subject: [PATCH 039/399] fix race conditions around header writing --- .../server/AsyncHttpServerResponseImpl.java | 107 ++++++++++-------- 1 file changed, 58 insertions(+), 49 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java index 3cc5d63f6..fbf4ba2a1 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java @@ -53,15 +53,16 @@ public AsyncSocket getSocket() { public void write(ByteBufferList bb) { if (bb.remaining() == 0) return; - writeInternal(bb); - } - private void writeInternal(ByteBufferList bb) { assert !mEnded; if (!mHasWritten) { initFirstWrite(); return; } + if (mSink == null) { + System.out.println("poop squat"); + return; + } mSink.write(bb); } @@ -72,36 +73,57 @@ void initFirstWrite() { return; mHasWritten = true; - String currentEncoding = mRawHeaders.get("Transfer-Encoding"); - if ("".equals(currentEncoding)) - mRawHeaders.removeAll("Transfer-Encoding"); - boolean canUseChunked = ("Chunked".equalsIgnoreCase(currentEncoding) || currentEncoding == null) - && !"close".equalsIgnoreCase(mRawHeaders.get("Connection")); - if (mContentLength < 0) { - String contentLength = mRawHeaders.get("Content-Length"); - if (!TextUtils.isEmpty(contentLength)) - mContentLength = Long.valueOf(contentLength); - } - if (mContentLength < 0 && canUseChunked) { - mRawHeaders.set("Transfer-Encoding", "Chunked"); - mSink = new ChunkedOutputFilter(mSocket); - } - else { - mSink = mSocket; - } - writeHeadInternal(); + + String statusLine = String.format("HTTP/1.1 %s %s", code, AsyncHttpServer.getResponseCodeDescription(code)); + String rh = mRawHeaders.toPrefixString(statusLine); + Util.writeAll(mSocket, rh.getBytes(), new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + String currentEncoding = mRawHeaders.get("Transfer-Encoding"); + if ("".equals(currentEncoding)) + mRawHeaders.removeAll("Transfer-Encoding"); + boolean canUseChunked = ("Chunked".equalsIgnoreCase(currentEncoding) || currentEncoding == null) + && !"close".equalsIgnoreCase(mRawHeaders.get("Connection")); + if (mContentLength < 0) { + String contentLength = mRawHeaders.get("Content-Length"); + if (!TextUtils.isEmpty(contentLength)) + mContentLength = Long.valueOf(contentLength); + } + if (mContentLength < 0 && canUseChunked) { + mRawHeaders.set("Transfer-Encoding", "Chunked"); + ChunkedOutputFilter chunked = new ChunkedOutputFilter(mSocket); + chunked.setMaxBuffer(0); + mSink = chunked; + } + else { + mSink = mSocket; + } + + mSink.setClosedCallback(closedCallback); + closedCallback = null; + mSink.setWriteableCallback(writable); + if (writable != null) { + writable.onWriteable(); + writable = null; + } + } + }); } + WritableCallback writable; @Override public void setWriteableCallback(WritableCallback handler) { - initFirstWrite(); - mSink.setWriteableCallback(handler); + if (mSink != null) + mSink.setWriteableCallback(handler); + else + writable = handler; } @Override public WritableCallback getWriteableCallback() { - initFirstWrite(); - return mSink.getWriteableCallback(); + if (mSink != null) + return mSink.getWriteableCallback(); + return writable; } @Override @@ -120,37 +142,18 @@ else if (!mHasWritten) { onEnd(); } } + else { + onEnd(); + } } - private boolean mHeadWritten = false; @Override public void writeHead() { initFirstWrite(); } - private void writeHeadInternal() { - assert !mHeadWritten; - mHeadWritten = true; - String statusLine = String.format("HTTP/1.1 %s %s", code, AsyncHttpServer.getResponseCodeDescription(code)); - String rh = mRawHeaders.toPrefixString(statusLine); - Util.writeAll(mSocket, rh.getBytes(), new CompletedCallback() { - @Override - public void onCompleted(Exception ex) { - // TODO: HACK!!! - // this really needs to be fixed. Not sure how to deal w/ writehead and - // first write - if (mSink instanceof BufferedDataSink) - ((BufferedDataSink)mSink).setDataSink(mSocket); - WritableCallback writableCallback = getWriteableCallback(); - if (writableCallback != null) - writableCallback.onWriteable(); - } - }); - } - @Override public void setContentType(String contentType) { - assert !mHeadWritten; mRawHeaders.set("Content-Type", contentType); } @@ -302,14 +305,20 @@ public boolean isOpen() { return mSocket.isOpen(); } + CompletedCallback closedCallback; @Override public void setClosedCallback(CompletedCallback handler) { - mSink.setClosedCallback(handler); + if (mSink != null) + mSink.setClosedCallback(handler); + else + closedCallback = handler; } @Override public CompletedCallback getClosedCallback() { - return mSink.getClosedCallback(); + if (mSink != null) + return mSink.getClosedCallback(); + return closedCallback; } @Override From ad5709a17b41306f76fc446673bb5779fcd98666 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 23 Jul 2014 23:52:42 -0700 Subject: [PATCH 040/399] Fix header chunking bug in AsyncHttpServerResponseImpl. Fix resume bugs in ChunkedInputFilter. --- .../async/AsyncSSLSocketWrapper.java | 11 ---- .../async/http/filter/ChunkedInputFilter.java | 9 ++-- .../server/AsyncHttpServerResponseImpl.java | 52 ++++++++++++------- 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 54138960d..0f777e6ea 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -140,17 +140,6 @@ public void onWriteable() { } }); - - // here's the stack of emitters - // ssl emitter - // buffered data emitter - // socket - - // ssl emitter needs a buffered emitter - // in case there is an underflow. - // buffered emitter will read from the socket, - // and replay data forever. - // on pause, the emitter is paused to prevent the buffered // socket and itself from firing. // on resume, emitter is resumed, ssl buffer is flushed as well diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedInputFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedInputFilter.java index 652e907d1..f049a066e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedInputFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedInputFilter.java @@ -43,6 +43,7 @@ protected void report(Exception e) { super.report(e); } + ByteBufferList pending = new ByteBufferList(); @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { try { @@ -82,12 +83,8 @@ else if (c >= 'A' && c <= 'F') } if (reading == 0) break; - ByteBufferList chunk = bb.get(reading); - int newRemaining = bb.remaining(); - assert remaining == chunk.remaining() + bb.remaining(); - assert reading == chunk.remaining(); - Util.emitAllData(this, chunk); - assert newRemaining == bb.remaining(); + bb.get(pending, reading); + Util.emitAllData(this, pending); break; case CHUNK_CR: if (!checkCR(bb.getByteChar())) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java index fbf4ba2a1..cdbfa5cc0 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java @@ -51,18 +51,21 @@ public AsyncSocket getSocket() { @Override public void write(ByteBufferList bb) { - if (bb.remaining() == 0) - return; - + // order is important here... assert !mEnded; - if (!mHasWritten) { + // do the header write... this will call onWritable, which may be reentrant + if (!mHasWritten) initFirstWrite(); + + // now check to see if the list is empty. reentrancy may cause it to empty itself. + if (bb.remaining() == 0) return; - } - if (mSink == null) { - System.out.println("poop squat"); + + // null sink means that the header has not finished writing + if (mSink == null) return; - } + + // can successfully write! mSink.write(bb); } @@ -74,23 +77,32 @@ void initFirstWrite() { mHasWritten = true; + final boolean isChunked; + String currentEncoding = mRawHeaders.get("Transfer-Encoding"); + if ("".equals(currentEncoding)) + mRawHeaders.removeAll("Transfer-Encoding"); + boolean canUseChunked = ("Chunked".equalsIgnoreCase(currentEncoding) || currentEncoding == null) + && !"close".equalsIgnoreCase(mRawHeaders.get("Connection")); + if (mContentLength < 0) { + String contentLength = mRawHeaders.get("Content-Length"); + if (!TextUtils.isEmpty(contentLength)) + mContentLength = Long.valueOf(contentLength); + } + if (mContentLength < 0 && canUseChunked) { + mRawHeaders.set("Transfer-Encoding", "Chunked"); + isChunked = true; + } + else { + isChunked = false; + } + String statusLine = String.format("HTTP/1.1 %s %s", code, AsyncHttpServer.getResponseCodeDescription(code)); String rh = mRawHeaders.toPrefixString(statusLine); + Util.writeAll(mSocket, rh.getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { - String currentEncoding = mRawHeaders.get("Transfer-Encoding"); - if ("".equals(currentEncoding)) - mRawHeaders.removeAll("Transfer-Encoding"); - boolean canUseChunked = ("Chunked".equalsIgnoreCase(currentEncoding) || currentEncoding == null) - && !"close".equalsIgnoreCase(mRawHeaders.get("Connection")); - if (mContentLength < 0) { - String contentLength = mRawHeaders.get("Content-Length"); - if (!TextUtils.isEmpty(contentLength)) - mContentLength = Long.valueOf(contentLength); - } - if (mContentLength < 0 && canUseChunked) { - mRawHeaders.set("Transfer-Encoding", "Chunked"); + if (isChunked) { ChunkedOutputFilter chunked = new ChunkedOutputFilter(mSocket); chunked.setMaxBuffer(0); mSink = chunked; From c08b7a36ce3cfc3032819089554a74ae4fae5cd4 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 24 Jul 2014 00:19:29 -0700 Subject: [PATCH 041/399] Pump the writable callback in a posted method. --- AndroidAsync/src/com/koushikdutta/async/Util.java | 9 ++------- .../http/server/AsyncHttpServerResponseImpl.java | 13 +++++++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/Util.java b/AndroidAsync/src/com/koushikdutta/async/Util.java index c8df5b77f..5dc0359bc 100644 --- a/AndroidAsync/src/com/koushikdutta/async/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/Util.java @@ -4,6 +4,7 @@ import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.util.Allocator; +import com.koushikdutta.async.util.StreamUtility; import com.koushikdutta.async.wrapper.AsyncSocketWrapper; import com.koushikdutta.async.wrapper.DataEmitterWrapper; @@ -69,13 +70,7 @@ private void cleanup() { ds.setClosedCallback(null); ds.setWriteableCallback(null); pending.recycle(); - pending = null; - try { - is.close(); - } - catch (IOException e) { - e.printStackTrace(); - } + StreamUtility.closeQuietly(is); } ByteBufferList pending = new ByteBufferList(); Allocator allocator = new Allocator(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java index cdbfa5cc0..41c20cecd 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java @@ -114,10 +114,15 @@ public void onCompleted(Exception ex) { mSink.setClosedCallback(closedCallback); closedCallback = null; mSink.setWriteableCallback(writable); - if (writable != null) { - writable.onWriteable(); - writable = null; - } + writable = null; + getServer().post(new Runnable() { + @Override + public void run() { + WritableCallback wb = getWriteableCallback(); + if (wb != null) + wb.onWriteable(); + } + }); } }); } From c866d2bffa839b92c90bb389227fbe12b7883a7c Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 24 Jul 2014 00:45:57 -0700 Subject: [PATCH 042/399] potential fix for selector exception in L preview. https://github.com/koush/AndroidAsync/issues/196 --- AndroidAsync/src/com/koushikdutta/async/AsyncServer.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java index a1c7a476a..6d5e5575e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java @@ -126,7 +126,12 @@ private static void wakeup(final SelectorWrapper selector) { synchronousWorkers.execute(new Runnable() { @Override public void run() { - selector.wakeupOnce(); + try { + selector.wakeupOnce(); + } + catch (Exception e) { + Log.i(LOGTAG, "Selector Exception? L Preview?"); + } } }); } From 86fb8a6e63afd2212d017c84e19358baf83205ea Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 24 Jul 2014 00:46:14 -0700 Subject: [PATCH 043/399] potential fix for selector exception in L preview. https://github.com/koush/AndroidAsync/issues/196 --- AndroidAsync/src/com/koushikdutta/async/AsyncServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java index b399362ca..6d5e5575e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java @@ -130,7 +130,7 @@ public void run() { selector.wakeupOnce(); } catch (Exception e) { - Log.i(LOGTAG, "Selector shit the bed."); + Log.i(LOGTAG, "Selector Exception? L Preview?"); } } }); From c3618aa1598ac3de0d39f199274812c5782798de Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 24 Jul 2014 00:56:50 -0700 Subject: [PATCH 044/399] okhttp spdy --- .gitignore | 5 +- AndroidAsync/AndroidAsync-AndroidAsync.iml | 1 + .../async/http/spdy/okhttp/Handshake.java | 106 ++ .../http/spdy/okhttp/internal/BitArray.java | 177 ++++ .../spdy/okhttp/internal/NamedRunnable.java | 40 + .../async/http/spdy/okhttp/internal/Util.java | 226 +++++ .../spdy/okhttp/internal/spdy/ErrorCode.java | 89 ++ .../okhttp/internal/spdy/FrameReader.java | 140 +++ .../okhttp/internal/spdy/FrameWriter.java | 100 ++ .../spdy/okhttp/internal/spdy/Header.java | 57 ++ .../okhttp/internal/spdy/HeadersMode.java | 49 + .../okhttp/internal/spdy/HpackDraft08.java | 491 ++++++++++ .../okhttp/internal/spdy/Http20Draft13.java | 760 +++++++++++++++ .../spdy/okhttp/internal/spdy/Huffman.java | 225 +++++ .../internal/spdy/IncomingStreamHandler.java | 36 + .../internal/spdy/NameValueBlockReader.java | 119 +++ .../http/spdy/okhttp/internal/spdy/Ping.java | 71 ++ .../okhttp/internal/spdy/PushObserver.java | 96 ++ .../spdy/okhttp/internal/spdy/Settings.java | 223 +++++ .../http/spdy/okhttp/internal/spdy/Spdy3.java | 514 ++++++++++ .../okhttp/internal/spdy/SpdyConnection.java | 874 +++++++++++++++++ .../spdy/okhttp/internal/spdy/SpdyStream.java | 577 +++++++++++ .../spdy/okhttp/internal/spdy/Variant.java | 40 + .../async/http/spdy/okio/AsyncTimeout.java | 318 ++++++ .../async/http/spdy/okio/Base64.java | 147 +++ .../async/http/spdy/okio/Buffer.java | 911 ++++++++++++++++++ .../async/http/spdy/okio/BufferedSink.java | 82 ++ .../async/http/spdy/okio/BufferedSource.java | 171 ++++ .../async/http/spdy/okio/ByteString.java | 283 ++++++ .../async/http/spdy/okio/DeflaterSink.java | 150 +++ .../http/spdy/okio/ForwardingSource.java | 49 + .../async/http/spdy/okio/InflaterSource.java | 123 +++ .../async/http/spdy/okio/Okio.java | 194 ++++ .../http/spdy/okio/RealBufferedSink.java | 207 ++++ .../http/spdy/okio/RealBufferedSource.java | 301 ++++++ .../async/http/spdy/okio/Segment.java | 135 +++ .../async/http/spdy/okio/SegmentPool.java | 64 ++ .../async/http/spdy/okio/Sink.java | 66 ++ .../async/http/spdy/okio/Source.java | 78 ++ .../async/http/spdy/okio/Timeout.java | 153 +++ .../async/http/spdy/okio/Util.java | 72 ++ 41 files changed, 8517 insertions(+), 3 deletions(-) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/Handshake.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/BitArray.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/NamedRunnable.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/ErrorCode.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeadersMode.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Huffman.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/IncomingStreamHandler.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/NameValueBlockReader.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Ping.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/PushObserver.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyConnection.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyStream.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/AsyncTimeout.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Base64.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSink.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSource.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ByteString.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/DeflaterSink.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ForwardingSource.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Okio.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSink.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSource.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Sink.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Source.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Timeout.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Util.java diff --git a/.gitignore b/.gitignore index 30c8bb1d2..cc439fd96 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ gen build .idea/ .DS_Store - -okhttp -okio +okhttp/ +okio/ libs diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index f88736b56..83bc4a321 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -57,6 +57,7 @@ + diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/Handshake.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/Handshake.java new file mode 100644 index 000000000..b9ae5de7c --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/Handshake.java @@ -0,0 +1,106 @@ +package com.koushikdutta.async.http.spdy.okhttp; + +import com.koushikdutta.async.http.spdy.okhttp.internal.Util; + +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; + +/** + * A record of a TLS handshake. For HTTPS clients, the client is local + * and the remote server is its peer. + * + *

This value object describes a completed handshake. Use {@link + * javax.net.ssl.SSLSocketFactory} to set policy for new handshakes. + */ +public final class Handshake { + private final String cipherSuite; + private final List peerCertificates; + private final List localCertificates; + + private Handshake( + String cipherSuite, List peerCertificates, List localCertificates) { + this.cipherSuite = cipherSuite; + this.peerCertificates = peerCertificates; + this.localCertificates = localCertificates; + } + + public static Handshake get(SSLSession session) { + String cipherSuite = session.getCipherSuite(); + if (cipherSuite == null) throw new IllegalStateException("cipherSuite == null"); + + Certificate[] peerCertificates; + try { + peerCertificates = session.getPeerCertificates(); + } catch (SSLPeerUnverifiedException ignored) { + peerCertificates = null; + } + List peerCertificatesList = peerCertificates != null + ? Util.immutableList(peerCertificates) + : Collections.emptyList(); + + Certificate[] localCertificates = session.getLocalCertificates(); + List localCertificatesList = localCertificates != null + ? Util.immutableList(localCertificates) + : Collections.emptyList(); + + return new Handshake(cipherSuite, peerCertificatesList, localCertificatesList); + } + + public static Handshake get( + String cipherSuite, List peerCertificates, List localCertificates) { + if (cipherSuite == null) throw new IllegalArgumentException("cipherSuite == null"); + return new Handshake(cipherSuite, Util.immutableList(peerCertificates), + Util.immutableList(localCertificates)); + } + + /** Returns a cipher suite name like "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA". */ + public String cipherSuite() { + return cipherSuite; + } + + /** Returns a possibly-empty list of certificates that identify the remote peer. */ + public List peerCertificates() { + return peerCertificates; + } + + /** Returns the remote peer's principle, or null if that peer is anonymous. */ + public Principal peerPrincipal() { + return !peerCertificates.isEmpty() + ? ((X509Certificate) peerCertificates.get(0)).getSubjectX500Principal() + : null; + } + + /** Returns a possibly-empty list of certificates that identify this peer. */ + public List localCertificates() { + return localCertificates; + } + + /** Returns the local principle, or null if this peer is anonymous. */ + public Principal localPrincipal() { + return !localCertificates.isEmpty() + ? ((X509Certificate) localCertificates.get(0)).getSubjectX500Principal() + : null; + } + + @Override public boolean equals(Object other) { + if (!(other instanceof Handshake)) return false; + Handshake that = (Handshake) other; + return cipherSuite.equals(that.cipherSuite) + && peerCertificates.equals(that.peerCertificates) + && localCertificates.equals(that.localCertificates); + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + cipherSuite.hashCode(); + result = 31 * result + peerCertificates.hashCode(); + result = 31 * result + localCertificates.hashCode(); + return result; + } +} \ No newline at end of file diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/BitArray.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/BitArray.java new file mode 100644 index 000000000..5db2b6e20 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/BitArray.java @@ -0,0 +1,177 @@ +/* + * Copyright 2014 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.koushikdutta.async.http.spdy.okhttp.internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static java.lang.String.format; + +/** A simple bitset which supports left shifting. */ +public interface BitArray { + + void clear(); + + void set(int index); + + void toggle(int index); + + boolean get(int index); + + void shiftLeft(int count); + + /** Bit set that only supports settings bits 0 - 63. */ + public final class FixedCapacity implements BitArray { + long data = 0x0000000000000000L; + + @Override public void clear() { + data = 0x0000000000000000L; + } + + @Override public void set(int index) { + data |= (1L << checkInput(index)); + } + + @Override public void toggle(int index) { + data ^= (1L << checkInput(index)); + } + + @Override public boolean get(int index) { + return ((data >> checkInput(index)) & 1L) == 1; + } + + @Override public void shiftLeft(int count) { + data = data << checkInput(count); + } + + @Override public String toString() { + return Long.toBinaryString(data); + } + + public BitArray toVariableCapacity() { + return new VariableCapacity(this); + } + + private static int checkInput(int index) { + if (index < 0 || index > 63) { + throw new IllegalArgumentException(format("input must be between 0 and 63: %s", index)); + } + return index; + } + } + + /** Bit set that grows as needed. */ + public final class VariableCapacity implements BitArray { + + long[] data; + + // Start offset which allows for cheap shifting. Data is always kept on 64-bit bounds but we + // offset the outward facing index to support shifts without having to move the underlying bits. + private int start; // Valid values are [0..63] + + public VariableCapacity() { + data = new long[1]; + } + + private VariableCapacity(FixedCapacity small) { + data = new long[] {small.data, 0}; + } + + private void growToSize(int size) { + long[] newData = new long[size]; + if (data != null) { + System.arraycopy(data, 0, newData, 0, data.length); + } + data = newData; + } + + private int offsetOf(int index) { + index += start; + int offset = index / 64; + if (offset > data.length - 1) { + growToSize(offset + 1); + } + return offset; + } + + private int shiftOf(int index) { + return (index + start) % 64; + } + + @Override public void clear() { + Arrays.fill(data, 0); + } + + @Override public void set(int index) { + checkInput(index); + int offset = offsetOf(index); + data[offset] |= 1L << shiftOf(index); + } + + @Override public void toggle(int index) { + checkInput(index); + int offset = offsetOf(index); + data[offset] ^= 1L << shiftOf(index); + } + + @Override public boolean get(int index) { + checkInput(index); + int offset = offsetOf(index); + return (data[offset] & (1L << shiftOf(index))) != 0; + } + + @Override public void shiftLeft(int count) { + start -= checkInput(count); + if (start < 0) { + int arrayShift = (start / -64) + 1; + long[] newData = new long[data.length + arrayShift]; + System.arraycopy(data, 0, newData, arrayShift, data.length); + data = newData; + start = 64 + (start % 64); + } + } + + @Override public String toString() { + StringBuilder builder = new StringBuilder("{"); + List ints = toIntegerList(); + for (int i = 0, count = ints.size(); i < count; i++) { + if (i > 0) { + builder.append(','); + } + builder.append(ints.get(i)); + } + return builder.append('}').toString(); + } + + List toIntegerList() { + List ints = new ArrayList(); + for (int i = 0, count = data.length * 64 - start; i < count; i++) { + if (get(i)) { + ints.add(i); + } + } + return ints; + } + + private static int checkInput(int index) { + if (index < 0) { + throw new IllegalArgumentException(format("input must be a positive number: %s", index)); + } + return index; + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/NamedRunnable.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/NamedRunnable.java new file mode 100644 index 000000000..9d9555162 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/NamedRunnable.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.koushikdutta.async.http.spdy.okhttp.internal; + +/** + * Runnable implementation which always sets its thread name. + */ +public abstract class NamedRunnable implements Runnable { + private final String name; + + public NamedRunnable(String format, Object... args) { + this.name = String.format(format, args); + } + + @Override public final void run() { + String oldName = Thread.currentThread().getName(); + Thread.currentThread().setName(name); + try { + execute(); + } finally { + Thread.currentThread().setName(oldName); + } + } + + protected abstract void execute(); +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java new file mode 100644 index 000000000..a6e00f45a --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.koushikdutta.async.http.spdy.okhttp.internal; + +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; +import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okio.Source; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.URL; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadFactory; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +/** Junk drawer of utility methods. */ +public final class Util { + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + public static final String[] EMPTY_STRING_ARRAY = new String[0]; + + /** A cheap and type-safe constant for the US-ASCII Charset. */ + public static final Charset US_ASCII = Charset.forName("US-ASCII"); + + /** A cheap and type-safe constant for the UTF-8 Charset. */ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + private Util() { + } + + public static int getEffectivePort(URI uri) { + return getEffectivePort(uri.getScheme(), uri.getPort()); + } + + public static int getEffectivePort(URL url) { + return getEffectivePort(url.getProtocol(), url.getPort()); + } + + private static int getEffectivePort(String scheme, int specifiedPort) { + return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme); + } + + public static int getDefaultPort(String protocol) { + if ("http".equals(protocol)) return 80; + if ("https".equals(protocol)) return 443; + return -1; + } + + public static void checkOffsetAndCount(long arrayLength, long offset, long count) { + if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + /** Returns true if two possibly-null objects are equal. */ + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Closes {@code closeable}, ignoring any checked exceptions. Does nothing + * if {@code closeable} is null. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Closes {@code socket}, ignoring any checked exceptions. Does nothing if + * {@code socket} is null. + */ + public static void closeQuietly(Socket socket) { + if (socket != null) { + try { + socket.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Closes {@code 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. + */ + public static void closeAll(Closeable a, Closeable b) throws IOException { + Throwable thrown = null; + try { + a.close(); + } catch (Throwable e) { + thrown = e; + } + try { + b.close(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + if (thrown == null) return; + if (thrown instanceof IOException) throw (IOException) thrown; + if (thrown instanceof RuntimeException) throw (RuntimeException) thrown; + if (thrown instanceof Error) throw (Error) thrown; + throw new AssertionError(thrown); + } + + /** + * Deletes the contents of {@code dir}. Throws an IOException if any file + * could not be deleted, or if {@code dir} is not a readable directory. + */ + public static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IOException("not a readable directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + /** Reads until {@code in} is exhausted or the timeout has elapsed. */ + public static boolean skipAll(Source in, int timeoutMillis) throws IOException { + // TODO: Implement deadlines everywhere so they can do this work. + long startNanos = System.nanoTime(); + Buffer skipBuffer = new Buffer(); + while (NANOSECONDS.toMillis(System.nanoTime() - startNanos) < timeoutMillis) { + long read = in.read(skipBuffer, 2048); + if (read == -1) return true; // Successfully exhausted the stream. + skipBuffer.clear(); + } + return false; // Ran out of time. + } + + /** 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 ByteString.of(md5bytes).hex(); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + /** Returns an immutable copy of {@code list}. */ + public static List immutableList(List list) { + return Collections.unmodifiableList(new ArrayList(list)); + } + + /** Returns an immutable list containing {@code elements}. */ + public static List immutableList(T... elements) { + return Collections.unmodifiableList(Arrays.asList(elements.clone())); + } + + public static ThreadFactory threadFactory(final String name, final boolean daemon) { + return new ThreadFactory() { + @Override public Thread newThread(Runnable runnable) { + Thread result = new Thread(runnable, name); + result.setDaemon(daemon); + return result; + } + }; + } + + public static List

headerEntries(String... elements) { + List
result = new ArrayList
(elements.length / 2); + for (int i = 0; i < elements.length; i += 2) { + result.add(new Header(elements[i], elements[i + 1])); + } + return result; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/ErrorCode.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/ErrorCode.java new file mode 100644 index 000000000..9a83aaae5 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/ErrorCode.java @@ -0,0 +1,89 @@ +/* + * 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +// http://tools.ietf.org/html/draft-ietf-httpbis-http2-13#section-7 +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), + + CONNECT_ERROR(10, -1, -1), + + ENHANCE_YOUR_CALM(11, -1, -1), + + INADEQUATE_SECURITY(12, -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/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java new file mode 100644 index 000000000..5305f632e --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java @@ -0,0 +1,140 @@ +/* + * 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.ByteString; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; + +/** Reads transport frames for SPDY/3 or HTTP/2. */ +public interface FrameReader extends Closeable { + boolean canProcessFrame(ByteBufferList bb); + void readConnectionPreface() throws IOException; + boolean nextFrame(Handler handler) throws IOException; + + public interface Handler { + void data(boolean inFinished, int streamId, BufferedSource source, 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 HEADERS and PUSH_PROMISE. + * + * @param outFinished true if the receiver should not send further frames. + * @param inFinished true if the sender will not send further frames. + * @param streamId the stream owning these headers. + * @param associatedStreamId the stream that triggered the sender to create + * this stream. + */ + void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, + List
headerBlock, HeadersMode headersMode); + void rstStream(int streamId, ErrorCode errorCode); + void settings(boolean clearPrevious, Settings settings); + + /** HTTP/2 only. */ + void ackSettings(); + + /** + * Read a connection-level ping from the peer. {@code ack} indicates this + * is a reply. Payload parameters are different between SPDY/3 and HTTP/2. + *

+ * In SPDY/3, only the first {@code payload1} parameter is set. If the + * reader is a client, it is an unsigned even number. Likewise, a server + * will receive an odd number. + *

+ * In HTTP/2, both {@code payload1} and {@code payload2} parameters are + * set. The data is opaque binary, and there are no rules on the content. + */ + void ping(boolean ack, int payload1, int payload2); + + /** + * The peer tells us to stop creating streams. It is safe to replay + * streams with {@code ID > lastGoodStreamId} on a new connection. In- + * flight streams with {@code ID <= lastGoodStreamId} can only be replayed + * on a new connection if they are idempotent. + * + * @param lastGoodStreamId the last stream ID the peer processed before + * sending this message. If {@code lastGoodStreamId} is zero, the peer + * processed no frames. + * @param errorCode reason for closing the connection. + * @param debugData only valid for HTTP/2; opaque debug data to send. + */ + void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData); + + /** + * Notifies that an additional {@code windowSizeIncrement} bytes can be + * sent on {@code streamId}, or the connection if {@code streamId} is zero. + */ + void windowUpdate(int streamId, long windowSizeIncrement); + + /** + * Called when reading a headers or priority frame. This may be used to + * change the stream's weight from the default (16) to a new value. + * + * @param streamId stream which has a priority change. + * @param streamDependency the stream ID this stream is dependent on. + * @param weight relative proportion of priority in [1..256]. + * @param exclusive inserts this stream ID as the sole child of + * {@code streamDependency}. + */ + void priority(int streamId, int streamDependency, int weight, boolean exclusive); + + /** + * HTTP/2 only. Receive a push promise header block. + *

+ * A push promise contains all the headers that pertain to a server-initiated + * request, and a {@code promisedStreamId} to which response frames will be + * delivered. Push promise frames are sent as a part of the response to + * {@code streamId}. + * + * @param streamId client-initiated stream ID. Must be an odd number. + * @param promisedStreamId server-initiated stream ID. Must be an even + * number. + * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, + * {@code :authority}, and (@code :path}. + */ + void pushPromise(int streamId, int promisedStreamId, List

requestHeaders) + throws IOException; + + /** + * HTTP/2 only. Expresses that resources for the connection or a client- + * initiated stream are available from a different network location or + * protocol configuration. + * + *

See alt-svc + * + * @param streamId when a client-initiated stream ID (odd number), the + * origin of this alternate service is the origin of the stream. When + * zero, the origin is specified in the {@code origin} parameter. + * @param origin when present, the + * origin is typically + * represented as a combination of scheme, host and port. When empty, + * the origin is that of the {@code streamId}. + * @param protocol an ALPN protocol, such as {@code h2}. + * @param host an IP address or hostname. + * @param port the IP port associated with the service. + * @param maxAge time in seconds that this alternative is considered fresh. + */ + void alternateService(int streamId, String origin, ByteString protocol, String host, int port, + long maxAge); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java new file mode 100644 index 000000000..f8781767c --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java @@ -0,0 +1,100 @@ +/* + * 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.http.spdy.okio.Buffer; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; + +/** Writes transport frames for SPDY/3 or HTTP/2. */ +public interface FrameWriter extends Closeable { + /** HTTP/2 only. */ + void connectionPreface() throws IOException; + void ackSettings() throws IOException; + + /** + * HTTP/2 only. Send a push promise header block. + *

+ * A push promise contains all the headers that pertain to a server-initiated + * request, and a {@code promisedStreamId} to which response frames will be + * delivered. Push promise frames are sent as a part of the response to + * {@code streamId}. The {@code promisedStreamId} has a priority of one + * greater than {@code streamId}. + * + * @param streamId client-initiated stream ID. Must be an odd number. + * @param promisedStreamId server-initiated stream ID. Must be an even + * number. + * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, + * {@code :authority}, and (@code :path}. + */ + void pushPromise(int streamId, int promisedStreamId, List

requestHeaders) + throws IOException; + + /** SPDY/3 only. */ + void flush() throws IOException; + void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, + List
headerBlock) throws IOException; + void synReply(boolean outFinished, int streamId, List
headerBlock) + throws IOException; + void headers(int streamId, List
headerBlock) throws IOException; + void rstStream(int streamId, ErrorCode errorCode) throws IOException; + + /** + * {@code data.length} may be longer than the max length of the variant's data frame. + * Implementations must send multiple frames as necessary. + * + * @param source the buffer to draw bytes from. May be null if byteCount is 0. + */ + void data(boolean outFinished, int streamId, Buffer source, int byteCount) throws IOException; + + void data(boolean outFinished, int streamId, Buffer source) throws IOException; + + /** Write okhttp's settings to the peer. */ + void settings(Settings okHttpSettings) throws IOException; + + /** + * Send a connection-level ping to the peer. {@code ack} indicates this is + * a reply. Payload parameters are different between SPDY/3 and HTTP/2. + *

+ * In SPDY/3, only the first {@code payload1} parameter is sent. If the + * sender is a client, it is an unsigned odd number. Likewise, a server + * will send an even number. + *

+ * In HTTP/2, both {@code payload1} and {@code payload2} parameters are + * sent. The data is opaque binary, and there are no rules on the content. + */ + void ping(boolean ack, int payload1, int payload2) throws IOException; + + /** + * Tell the peer to stop creating streams and that we last processed + * {@code lastGoodStreamId}, or zero if no streams were processed. + * + * @param lastGoodStreamId the last stream ID processed, or zero if no + * streams were processed. + * @param errorCode reason for closing the connection. + * @param debugData only valid for HTTP/2; opaque debug data to send. + */ + void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) throws IOException; + + /** + * Inform peer that an additional {@code windowSizeIncrement} bytes can be + * sent on {@code streamId}, or the connection if {@code streamId} is zero. + */ + void windowUpdate(int streamId, long windowSizeIncrement) throws IOException; +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java new file mode 100644 index 000000000..ba0244fcb --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java @@ -0,0 +1,57 @@ +package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; + + +import com.koushikdutta.async.http.spdy.okio.ByteString; + +/** HTTP header: the name is an ASCII string, but the value can be UTF-8. */ +public final class Header { + // Special header names defined in the SPDY and HTTP/2 specs. + public static final ByteString RESPONSE_STATUS = ByteString.encodeUtf8(":status"); + public static final ByteString TARGET_METHOD = ByteString.encodeUtf8(":method"); + public static final ByteString TARGET_PATH = ByteString.encodeUtf8(":path"); + public static final ByteString TARGET_SCHEME = ByteString.encodeUtf8(":scheme"); + public static final ByteString TARGET_AUTHORITY = ByteString.encodeUtf8(":authority"); // HTTP/2 + public static final ByteString TARGET_HOST = ByteString.encodeUtf8(":host"); // spdy/3 + public static final ByteString VERSION = ByteString.encodeUtf8(":version"); // spdy/3 + + /** Name in case-insensitive ASCII encoding. */ + public final ByteString name; + /** Value in UTF-8 encoding. */ + public final ByteString value; + final int hpackSize; + + // TODO: search for toLowerCase and consider moving logic here. + public Header(String name, String value) { + this(ByteString.encodeUtf8(name), ByteString.encodeUtf8(value)); + } + + public Header(ByteString name, String value) { + this(name, ByteString.encodeUtf8(value)); + } + + public Header(ByteString name, ByteString value) { + this.name = name; + this.value = value; + this.hpackSize = 32 + name.size() + value.size(); + } + + @Override public boolean equals(Object other) { + if (other instanceof Header) { + Header that = (Header) other; + return this.name.equals(that.name) + && this.value.equals(that.value); + } + return false; + } + + @Override public int hashCode() { + int result = 17; + result = 31 * result + name.hashCode(); + result = 31 * result + value.hashCode(); + return result; + } + + @Override public String toString() { + return String.format("%s: %s", name.utf8(), value.utf8()); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeadersMode.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeadersMode.java new file mode 100644 index 000000000..7ec54b58a --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +public 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/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java new file mode 100644 index 000000000..397736f71 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java @@ -0,0 +1,491 @@ +/* + * 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.http.spdy.okhttp.internal.BitArray; +import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okio.Okio; +import com.koushikdutta.async.http.spdy.okio.Source; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Read and write HPACK v08. + * + * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08 + * + * This implementation uses an array for the header table with a bitset for + * references. Dynamic entries are added to the array, starting in the last + * position moving forward. When the array fills, it is doubled. + */ +final class HpackDraft08 { + private static final int PREFIX_4_BITS = 0x0f; + private static final int PREFIX_6_BITS = 0x3f; + private static final int PREFIX_7_BITS = 0x7f; + + private static final Header[] STATIC_HEADER_TABLE = new Header[] { + new Header(Header.TARGET_AUTHORITY, ""), + new Header(Header.TARGET_METHOD, "GET"), + new Header(Header.TARGET_METHOD, "POST"), + new Header(Header.TARGET_PATH, "/"), + new Header(Header.TARGET_PATH, "/index.html"), + new Header(Header.TARGET_SCHEME, "http"), + new Header(Header.TARGET_SCHEME, "https"), + new Header(Header.RESPONSE_STATUS, "200"), + new Header(Header.RESPONSE_STATUS, "204"), + new Header(Header.RESPONSE_STATUS, "206"), + new Header(Header.RESPONSE_STATUS, "304"), + new Header(Header.RESPONSE_STATUS, "400"), + new Header(Header.RESPONSE_STATUS, "404"), + new Header(Header.RESPONSE_STATUS, "500"), + new Header("accept-charset", ""), + new Header("accept-encoding", "gzip, deflate"), + new Header("accept-language", ""), + new Header("accept-ranges", ""), + new Header("accept", ""), + new Header("access-control-allow-origin", ""), + new Header("age", ""), + new Header("allow", ""), + new Header("authorization", ""), + new Header("cache-control", ""), + new Header("content-disposition", ""), + new Header("content-encoding", ""), + new Header("content-language", ""), + new Header("content-length", ""), + new Header("content-location", ""), + new Header("content-range", ""), + new Header("content-type", ""), + new Header("cookie", ""), + new Header("date", ""), + new Header("etag", ""), + new Header("expect", ""), + new Header("expires", ""), + new Header("from", ""), + new Header("host", ""), + new Header("if-match", ""), + new Header("if-modified-since", ""), + new Header("if-none-match", ""), + new Header("if-range", ""), + new Header("if-unmodified-since", ""), + new Header("last-modified", ""), + new Header("link", ""), + new Header("location", ""), + new Header("max-forwards", ""), + new Header("proxy-authenticate", ""), + new Header("proxy-authorization", ""), + new Header("range", ""), + new Header("referer", ""), + new Header("refresh", ""), + new Header("retry-after", ""), + new Header("server", ""), + new Header("set-cookie", ""), + new Header("strict-transport-security", ""), + new Header("transfer-encoding", ""), + new Header("user-agent", ""), + new Header("vary", ""), + new Header("via", ""), + new Header("www-authenticate", "") + }; + + private HpackDraft08() { + } + + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-3.2 + static final class Reader { + + private final List

emittedHeaders = new ArrayList
(); + private final BufferedSource source; + + private int maxHeaderTableByteCountSetting; + private int maxHeaderTableByteCount; + // Visible for testing. + Header[] headerTable = new Header[8]; + // Array is populated back to front, so new entries always have lowest index. + int nextHeaderIndex = headerTable.length - 1; + int headerCount = 0; + + /** + * Set bit positions indicate {@code headerTable[pos]} should be emitted. + */ + // Using a BitArray as it has left-shift operator. + BitArray referencedHeaders = new BitArray.FixedCapacity(); + + /** + * Set bit positions indicate {@code headerTable[pos]} was already emitted. + */ + BitArray emittedReferencedHeaders = new BitArray.FixedCapacity(); + int headerTableByteCount = 0; + + Reader(int maxHeaderTableByteCountSetting, Source source) { + this.maxHeaderTableByteCountSetting = maxHeaderTableByteCountSetting; + this.maxHeaderTableByteCount = maxHeaderTableByteCountSetting; + this.source = Okio.buffer(source); + } + + int maxHeaderTableByteCount() { + return maxHeaderTableByteCount; + } + + /** + * Called by the reader when the peer sent a new header table size setting. + * While this establishes the maximum header table size, the + * {@link #maxHeaderTableByteCount} set during processing may limit the + * table size to a smaller amount. + *

Evicts entries or clears the table as needed. + */ + void maxHeaderTableByteCountSetting(int newMaxHeaderTableByteCountSetting) { + this.maxHeaderTableByteCountSetting = newMaxHeaderTableByteCountSetting; + this.maxHeaderTableByteCount = maxHeaderTableByteCountSetting; + adjustHeaderTableByteCount(); + } + + private void adjustHeaderTableByteCount() { + if (maxHeaderTableByteCount < headerTableByteCount) { + if (maxHeaderTableByteCount == 0) { + clearHeaderTable(); + } else { + evictToRecoverBytes(headerTableByteCount - maxHeaderTableByteCount); + } + } + } + + private void clearHeaderTable() { + clearReferenceSet(); + Arrays.fill(headerTable, null); + nextHeaderIndex = headerTable.length - 1; + headerCount = 0; + headerTableByteCount = 0; + } + + /** Returns the count of entries evicted. */ + private int evictToRecoverBytes(int bytesToRecover) { + int entriesToEvict = 0; + if (bytesToRecover > 0) { + // determine how many headers need to be evicted. + for (int j = headerTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) { + bytesToRecover -= headerTable[j].hpackSize; + headerTableByteCount -= headerTable[j].hpackSize; + headerCount--; + entriesToEvict++; + } + referencedHeaders.shiftLeft(entriesToEvict); + emittedReferencedHeaders.shiftLeft(entriesToEvict); + System.arraycopy(headerTable, nextHeaderIndex + 1, headerTable, + nextHeaderIndex + 1 + entriesToEvict, headerCount); + nextHeaderIndex += entriesToEvict; + } + return entriesToEvict; + } + + /** + * Read {@code byteCount} bytes of headers from the source stream into the + * set of emitted headers. This implementation does not propagate the never + * indexed flag of a header. + */ + void readHeaders() throws IOException { + while (!source.exhausted()) { + int b = source.readByte() & 0xff; + if (b == 0x80) { // 10000000 + throw new IOException("index == 0"); + } else if ((b & 0x80) == 0x80) { // 1NNNNNNN + int index = readInt(b, PREFIX_7_BITS); + readIndexedHeader(index - 1); + } else if (b == 0x40) { // 01000000 + readLiteralHeaderWithIncrementalIndexingNewName(); + } else if ((b & 0x40) == 0x40) { // 01NNNNNN + int index = readInt(b, PREFIX_6_BITS); + readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); + } else if ((b & 0x20) == 0x20) { // 001NNNNN + if ((b & 0x10) == 0x10) { // 0011NNNN + if ((b & 0x0f) != 0) throw new IOException("Invalid header table state change " + b); + clearReferenceSet(); // 00110000 + } else { // 0010NNNN + maxHeaderTableByteCount = readInt(b, PREFIX_4_BITS); + if (maxHeaderTableByteCount < 0 + || maxHeaderTableByteCount > maxHeaderTableByteCountSetting) { + throw new IOException("Invalid header table byte count " + maxHeaderTableByteCount); + } + adjustHeaderTableByteCount(); + } + } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit. + readLiteralHeaderWithoutIndexingNewName(); + } else { // 000?NNNN - Ignore never indexed bit. + int index = readInt(b, PREFIX_4_BITS); + readLiteralHeaderWithoutIndexingIndexedName(index - 1); + } + } + } + + private void clearReferenceSet() { + referencedHeaders.clear(); + emittedReferencedHeaders.clear(); + } + + void emitReferenceSet() { + for (int i = headerTable.length - 1; i != nextHeaderIndex; --i) { + if (referencedHeaders.get(i) && !emittedReferencedHeaders.get(i)) { + emittedHeaders.add(headerTable[i]); + } + } + } + + /** + * Returns all headers emitted since they were last cleared, then clears the + * emitted headers. + */ + List

getAndReset() { + List
result = new ArrayList
(emittedHeaders); + emittedHeaders.clear(); + emittedReferencedHeaders.clear(); + return result; + } + + private void readIndexedHeader(int index) throws IOException { + if (isStaticHeader(index)) { + index -= headerCount; + if (index > STATIC_HEADER_TABLE.length - 1) { + throw new IOException("Header index too large " + (index + 1)); + } + Header staticEntry = STATIC_HEADER_TABLE[index]; + if (maxHeaderTableByteCount == 0) { + emittedHeaders.add(staticEntry); + } else { + insertIntoHeaderTable(-1, staticEntry); + } + } else { + int headerTableIndex = headerTableIndex(index); + if (!referencedHeaders.get(headerTableIndex)) { // When re-referencing, emit immediately. + emittedHeaders.add(headerTable[headerTableIndex]); + emittedReferencedHeaders.set(headerTableIndex); + } + referencedHeaders.toggle(headerTableIndex); + } + } + + // referencedHeaders is relative to nextHeaderIndex + 1. + private int headerTableIndex(int index) { + return nextHeaderIndex + 1 + index; + } + + private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException { + ByteString name = getName(index); + ByteString value = readByteString(); + emittedHeaders.add(new Header(name, value)); + } + + private void readLiteralHeaderWithoutIndexingNewName() throws IOException { + ByteString name = checkLowercase(readByteString()); + ByteString value = readByteString(); + emittedHeaders.add(new Header(name, value)); + } + + private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex) + throws IOException { + ByteString name = getName(nameIndex); + ByteString value = readByteString(); + insertIntoHeaderTable(-1, new Header(name, value)); + } + + private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException { + ByteString name = checkLowercase(readByteString()); + ByteString value = readByteString(); + insertIntoHeaderTable(-1, new Header(name, value)); + } + + private ByteString getName(int index) { + if (isStaticHeader(index)) { + return STATIC_HEADER_TABLE[index - headerCount].name; + } else { + return headerTable[headerTableIndex(index)].name; + } + } + + private boolean isStaticHeader(int index) { + return index >= headerCount; + } + + /** index == -1 when new. */ + private void insertIntoHeaderTable(int index, Header entry) { + int delta = entry.hpackSize; + if (index != -1) { // Index -1 == new header. + delta -= headerTable[headerTableIndex(index)].hpackSize; + } + + // if the new or replacement header is too big, drop all entries. + if (delta > maxHeaderTableByteCount) { + clearHeaderTable(); + // emit the large header to the callback. + emittedHeaders.add(entry); + return; + } + + // Evict headers to the required length. + int bytesToRecover = (headerTableByteCount + delta) - maxHeaderTableByteCount; + int entriesEvicted = evictToRecoverBytes(bytesToRecover); + + if (index == -1) { // Adding a value to the header table. + if (headerCount + 1 > headerTable.length) { // Need to grow the header table. + Header[] doubled = new Header[headerTable.length * 2]; + System.arraycopy(headerTable, 0, doubled, headerTable.length, headerTable.length); + if (doubled.length == 64) { + referencedHeaders = ((BitArray.FixedCapacity) referencedHeaders).toVariableCapacity(); + emittedReferencedHeaders = + ((BitArray.FixedCapacity) emittedReferencedHeaders).toVariableCapacity(); + } + referencedHeaders.shiftLeft(headerTable.length); + emittedReferencedHeaders.shiftLeft(headerTable.length); + nextHeaderIndex = headerTable.length - 1; + headerTable = doubled; + } + index = nextHeaderIndex--; + referencedHeaders.set(index); + headerTable[index] = entry; + headerCount++; + } else { // Replace value at same position. + index += headerTableIndex(index) + entriesEvicted; + referencedHeaders.set(index); + headerTable[index] = entry; + } + headerTableByteCount += delta; + } + + private int readByte() throws IOException { + return source.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 potentially Huffman encoded byte string. */ + ByteString readByteString() throws IOException { + int firstByte = readByte(); + boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN + int length = readInt(firstByte, PREFIX_7_BITS); + + if (huffmanDecode) { + return ByteString.of(Huffman.get().decode(source.readByteArray(length))); + } else { + return source.readByteString(length); + } + } + } + + private static final Map NAME_TO_FIRST_INDEX = nameToFirstIndex(); + + private static Map nameToFirstIndex() { + Map result = new LinkedHashMap(STATIC_HEADER_TABLE.length); + for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) { + if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) { + result.put(STATIC_HEADER_TABLE[i].name, i); + } + } + return Collections.unmodifiableMap(result); + } + + static final class Writer { + private final Buffer out; + + Writer(Buffer out) { + this.out = out; + } + + /** This does not use "never indexed" semantics for sensitive headers. */ + // https://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.3.3 + void writeHeaders(List
headerBlock) throws IOException { + // TODO: implement index tracking + for (int i = 0, size = headerBlock.size(); i < size; i++) { + ByteString name = headerBlock.get(i).name.toAsciiLowercase(); + Integer staticIndex = NAME_TO_FIRST_INDEX.get(name); + if (staticIndex != null) { + // Literal Header Field without Indexing - Indexed Name. + writeInt(staticIndex + 1, PREFIX_4_BITS, 0); + writeByteString(headerBlock.get(i).value); + } else { + out.writeByte(0x00); // Literal Header without Indexing - New Name. + writeByteString(name); + writeByteString(headerBlock.get(i).value); + } + } + } + + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.1.1 + void writeInt(int value, int prefixMask, int bits) throws IOException { + // Write the raw value for a single byte value. + if (value < prefixMask) { + out.writeByte(bits | value); + return; + } + + // Write the mask to start a multibyte value. + out.writeByte(bits | prefixMask); + value -= prefixMask; + + // Write 7 bits at a time 'til we're done. + while (value >= 0x80) { + int b = value & 0x7f; + out.writeByte(b | 0x80); + value >>>= 7; + } + out.writeByte(value); + } + + void writeByteString(ByteString data) throws IOException { + writeInt(data.size(), PREFIX_7_BITS, 0); + out.write(data); + } + } + + /** + * An HTTP/2 response cannot contain uppercase header characters and must + * be treated as malformed. + */ + private static ByteString checkLowercase(ByteString name) throws IOException { + for (int i = 0, length = name.size(); i < length; i++) { + byte c = name.getByte(i); + if (c >= 'A' && c <= 'Z') { + throw new IOException("PROTOCOL_ERROR response malformed: mixed case name: " + name.utf8()); + } + } + return name; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java new file mode 100644 index 000000000..88958ef15 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java @@ -0,0 +1,760 @@ +/* + * 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.http.Protocol; +import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.http.spdy.okio.BufferedSink; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okio.Source; +import com.koushikdutta.async.http.spdy.okio.Timeout; + +import java.io.IOException; +import java.nio.ByteOrder; +import java.util.List; +import java.util.logging.Logger; + +import static com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Http20Draft13.FrameLogger.formatHeader; +import static com.koushikdutta.async.http.spdy.okio.ByteString.EMPTY; +import static java.lang.String.format; +import static java.util.logging.Level.FINE; + +/** + * Read and write HTTP/2 v13 frames. + *

http://tools.ietf.org/html/draft-ietf-httpbis-http2-13 + */ +public final class Http20Draft13 implements Variant { + private static final Logger logger = Logger.getLogger(Http20Draft13.class.getName()); + + @Override public Protocol getProtocol() { + return Protocol.HTTP_2; + } + + private static final ByteString CONNECTION_PREFACE + = ByteString.encodeUtf8("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"); + + static final int MAX_FRAME_SIZE = 0x3fff; // 16383 + + static final byte TYPE_DATA = 0x0; + static final byte TYPE_HEADERS = 0x1; + static final byte TYPE_PRIORITY = 0x2; + static final byte TYPE_RST_STREAM = 0x3; + static final byte TYPE_SETTINGS = 0x4; + static final byte TYPE_PUSH_PROMISE = 0x5; + static final byte TYPE_PING = 0x6; + static final byte TYPE_GOAWAY = 0x7; + static final byte TYPE_WINDOW_UPDATE = 0x8; + static final byte TYPE_CONTINUATION = 0x9; + + static final byte FLAG_NONE = 0x0; + static final byte FLAG_ACK = 0x1; // Used for settings and ping. + static final byte FLAG_END_STREAM = 0x1; // Used for headers and data. + static final byte FLAG_END_SEGMENT = 0x2; + static final byte FLAG_END_HEADERS = 0x4; // Used for headers and continuation. + static final byte FLAG_END_PUSH_PROMISE = 0x4; + static final byte FLAG_PADDED = 0x8; // Used for headers and data. + static final byte FLAG_PRIORITY = 0x20; // Used for headers. + static final byte FLAG_COMPRESSED = 0x20; // Used for data. + + /** + * Creates a frame reader with max header table size of 4096 and data frame + * compression disabled. + */ + @Override public FrameReader newReader(BufferedSource source, boolean client) { + return new Reader(source, 4096, client); + } + + @Override public FrameWriter newWriter(BufferedSink sink, boolean client) { + return new Writer(sink, client); + } + + @Override public int maxFrameSize() { + return MAX_FRAME_SIZE; + } + + static final class Reader implements FrameReader { + private final BufferedSource source; + private final ContinuationSource continuation; + private final boolean client; + + // Visible for testing. + final HpackDraft08.Reader hpackReader; + + @Override + public boolean canProcessFrame(ByteBufferList bb) { + if (bb.remaining() < 4) + return false; + bb.order(ByteOrder.BIG_ENDIAN); + int w1 = bb.peekInt(); + + short length = (short) ((w1 & 0x3fff0000) >> 16); // 14-bit unsigned == MAX_FRAME_SIZE + return bb.remaining() >= 8 + length; + } + + Reader(BufferedSource source, int headerTableSize, boolean client) { + this.source = source; + this.client = client; + this.continuation = new ContinuationSource(this.source); + this.hpackReader = new HpackDraft08.Reader(headerTableSize, continuation); + } + + @Override public void readConnectionPreface() throws IOException { + if (client) return; // Nothing to read; servers doesn't send a connection preface! + ByteString connectionPreface = source.readByteString(CONNECTION_PREFACE.size()); + if (logger.isLoggable(FINE)) logger.fine(format("<< CONNECTION %s", connectionPreface.hex())); + if (!CONNECTION_PREFACE.equals(connectionPreface)) { + throw ioException("Expected a connection header but was %s", connectionPreface.utf8()); + } + } + + @Override public boolean nextFrame(Handler handler) throws IOException { + int w1; + int w2; + try { + w1 = source.readInt(); + w2 = source.readInt(); + } catch (IOException e) { + return false; // This might be a normal socket close. + } + + // boolean r = (w1 & 0xc0000000) != 0; // Reserved: Ignore first 2 bits. + short length = (short) ((w1 & 0x3fff0000) >> 16); // 14-bit unsigned == MAX_FRAME_SIZE + byte type = (byte) ((w1 & 0xff00) >> 8); + byte flags = (byte) (w1 & 0xff); + // boolean r = (w2 & 0x80000000) != 0; // Reserved: Ignore first bit. + int streamId = (w2 & 0x7fffffff); // 31-bit opaque identifier. + if (logger.isLoggable(FINE)) logger.fine(formatHeader(true, streamId, length, type, flags)); + + switch (type) { + case TYPE_DATA: + readData(handler, length, flags, streamId); + break; + + case TYPE_HEADERS: + readHeaders(handler, length, flags, streamId); + break; + + case TYPE_PRIORITY: + readPriority(handler, length, flags, streamId); + break; + + case TYPE_RST_STREAM: + readRstStream(handler, length, flags, streamId); + break; + + case TYPE_SETTINGS: + readSettings(handler, length, flags, streamId); + break; + + case TYPE_PUSH_PROMISE: + readPushPromise(handler, length, flags, streamId); + break; + + case TYPE_PING: + readPing(handler, length, flags, streamId); + break; + + case TYPE_GOAWAY: + readGoAway(handler, length, flags, streamId); + break; + + case TYPE_WINDOW_UPDATE: + readWindowUpdate(handler, length, flags, streamId); + break; + + default: + // Implementations MUST discard frames that have unknown or unsupported types. + source.skip(length); + } + return true; + } + + private void readHeaders(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0"); + + boolean endStream = (flags & FLAG_END_STREAM) != 0; + + short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; + + if ((flags & FLAG_PRIORITY) != 0) { + readPriority(handler, streamId); + length -= 5; // account for above read. + } + + length = lengthWithoutPadding(length, flags, padding); + + List

headerBlock = readHeaderBlock(length, padding, flags, streamId); + + handler.headers(false, endStream, streamId, -1, headerBlock, HeadersMode.HTTP_20_HEADERS); + } + + private List
readHeaderBlock(short length, short padding, byte flags, int streamId) + throws IOException { + continuation.length = continuation.left = length; + continuation.padding = padding; + continuation.flags = flags; + continuation.streamId = streamId; + + hpackReader.readHeaders(); + hpackReader.emitReferenceSet(); + // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20. + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3 + return hpackReader.getAndReset(); + } + + private void readData(Handler handler, short length, byte flags, int streamId) + throws IOException { + // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED + boolean inFinished = (flags & FLAG_END_STREAM) != 0; + boolean gzipped = (flags & FLAG_COMPRESSED) != 0; + if (gzipped) { + throw ioException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA"); + } + + short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; + length = lengthWithoutPadding(length, flags, padding); + + handler.data(inFinished, streamId, source, length); + source.skip(padding); + } + + private void readPriority(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (length != 5) throw ioException("TYPE_PRIORITY length: %d != 5", length); + if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0"); + readPriority(handler, streamId); + } + + private void readPriority(Handler handler, int streamId) throws IOException { + int w1 = source.readInt(); + boolean exclusive = (w1 & 0x80000000) != 0; + int streamDependency = (w1 & 0x7fffffff); + int weight = (source.readByte() & 0xff) + 1; + handler.priority(streamId, streamDependency, weight, exclusive); + } + + private void readRstStream(Handler handler, short length, byte flags, 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 = source.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, short length, byte flags, int streamId) + throws IOException { + if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0"); + if ((flags & FLAG_ACK) != 0) { + if (length != 0) throw ioException("FRAME_SIZE_ERROR ack frame should be empty!"); + handler.ackSettings(); + return; + } + + if (length % 6 != 0) throw ioException("TYPE_SETTINGS length %% 6 != 0: %s", length); + Settings settings = new Settings(); + for (int i = 0; i < length; i += 6) { + short id = source.readShort(); + int value = source.readInt(); + + switch (id) { + case 1: // SETTINGS_HEADER_TABLE_SIZE + break; + case 2: // SETTINGS_ENABLE_PUSH + if (value != 0 && value != 1) { + throw ioException("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1"); + } + break; + case 3: // SETTINGS_MAX_CONCURRENT_STREAMS + id = 4; // Renumbered in draft 10. + break; + case 4: // SETTINGS_INITIAL_WINDOW_SIZE + id = 7; // Renumbered in draft 10. + if (value < 0) { + throw ioException("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1"); + } + break; + case 5: // SETTINGS_COMPRESS_DATA + break; + default: + throw ioException("PROTOCOL_ERROR invalid settings id: %s", id); + } + settings.set(id, 0, value); + } + handler.settings(false, settings); + if (settings.getHeaderTableSize() >= 0) { + hpackReader.maxHeaderTableByteCountSetting(settings.getHeaderTableSize()); + } + } + + private void readPushPromise(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (streamId == 0) { + throw ioException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0"); + } + short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; + int promisedStreamId = source.readInt() & 0x7fffffff; + length -= 4; // account for above read. + length = lengthWithoutPadding(length, flags, padding); + List
headerBlock = readHeaderBlock(length, padding, flags, streamId); + handler.pushPromise(streamId, promisedStreamId, headerBlock); + } + + private void readPing(Handler handler, short length, byte flags, 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 = source.readInt(); + int payload2 = source.readInt(); + boolean ack = (flags & FLAG_ACK) != 0; + handler.ping(ack, payload1, payload2); + } + + private void readGoAway(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length); + if (streamId != 0) throw ioException("TYPE_GOAWAY streamId != 0"); + int lastStreamId = source.readInt(); + int errorCodeInt = source.readInt(); + int opaqueDataLength = length - 8; + ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); + if (errorCode == null) { + throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt); + } + ByteString debugData = EMPTY; + if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection. + debugData = source.readByteString(opaqueDataLength); + } + handler.goAway(lastStreamId, errorCode, debugData); + } + + private void readWindowUpdate(Handler handler, short length, byte flags, int streamId) + throws IOException { + if (length != 4) throw ioException("TYPE_WINDOW_UPDATE length !=4: %s", length); + long increment = (source.readInt() & 0x7fffffffL); + if (increment == 0) throw ioException("windowSizeIncrement was 0", increment); + handler.windowUpdate(streamId, increment); + } + + @Override public void close() throws IOException { + source.close(); + } + } + + static final class Writer implements FrameWriter { + private final BufferedSink sink; + private final boolean client; + private final Buffer hpackBuffer; + private final HpackDraft08.Writer hpackWriter; + private boolean closed; + + Writer(BufferedSink sink, boolean client) { + this.sink = sink; + this.client = client; + this.hpackBuffer = new Buffer(); + this.hpackWriter = new HpackDraft08.Writer(hpackBuffer); + } + + @Override public synchronized void flush() throws IOException { + if (closed) throw new IOException("closed"); + sink.flush(); + } + + @Override public synchronized void ackSettings() throws IOException { + if (closed) throw new IOException("closed"); + int length = 0; + byte type = TYPE_SETTINGS; + byte flags = FLAG_ACK; + int streamId = 0; + frameHeader(streamId, length, type, flags); + sink.flush(); + } + + @Override public synchronized void connectionPreface() throws IOException { + if (closed) throw new IOException("closed"); + if (!client) return; // Nothing to write; servers don't send connection headers! + if (logger.isLoggable(FINE)) { + logger.fine(format(">> CONNECTION %s", CONNECTION_PREFACE.hex())); + } + sink.write(CONNECTION_PREFACE.toByteArray()); + sink.flush(); + } + + @Override public synchronized void synStream(boolean outFinished, boolean inFinished, + int streamId, int associatedStreamId, List
headerBlock) + throws IOException { + if (inFinished) throw new UnsupportedOperationException(); + if (closed) throw new IOException("closed"); + headers(outFinished, streamId, headerBlock); + } + + @Override public synchronized void synReply(boolean outFinished, int streamId, + List
headerBlock) throws IOException { + if (closed) throw new IOException("closed"); + headers(outFinished, streamId, headerBlock); + } + + @Override public synchronized void headers(int streamId, List
headerBlock) + throws IOException { + if (closed) throw new IOException("closed"); + headers(false, streamId, headerBlock); + } + + @Override public synchronized void pushPromise(int streamId, int promisedStreamId, + List
requestHeaders) throws IOException { + if (closed) throw new IOException("closed"); + if (hpackBuffer.size() != 0) throw new IllegalStateException(); + hpackWriter.writeHeaders(requestHeaders); + + long byteCount = hpackBuffer.size(); + int length = (int) Math.min(MAX_FRAME_SIZE - 4, byteCount); + byte type = TYPE_PUSH_PROMISE; + byte flags = byteCount == length ? FLAG_END_HEADERS : 0; + frameHeader(streamId, length + 4, type, flags); + sink.writeInt(promisedStreamId & 0x7fffffff); + sink.write(hpackBuffer, length); + + if (byteCount > length) writeContinuationFrames(streamId, byteCount - length); + } + + void headers(boolean outFinished, int streamId, List
headerBlock) throws IOException { + if (closed) throw new IOException("closed"); + if (hpackBuffer.size() != 0) throw new IllegalStateException(); + hpackWriter.writeHeaders(headerBlock); + + long byteCount = hpackBuffer.size(); + int length = (int) Math.min(MAX_FRAME_SIZE, byteCount); + byte type = TYPE_HEADERS; + byte flags = byteCount == length ? FLAG_END_HEADERS : 0; + if (outFinished) flags |= FLAG_END_STREAM; + frameHeader(streamId, length, type, flags); + sink.write(hpackBuffer, length); + + if (byteCount > length) writeContinuationFrames(streamId, byteCount - length); + } + + private void writeContinuationFrames(int streamId, long byteCount) throws IOException { + while (byteCount > 0) { + int length = (int) Math.min(MAX_FRAME_SIZE, byteCount); + byteCount -= length; + frameHeader(streamId, length, TYPE_CONTINUATION, byteCount == 0 ? FLAG_END_HEADERS : 0); + sink.write(hpackBuffer, length); + } + } + + @Override public synchronized void rstStream(int streamId, ErrorCode errorCode) + throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException(); + + int length = 4; + byte type = TYPE_RST_STREAM; + byte flags = FLAG_NONE; + frameHeader(streamId, length, type, flags); + sink.writeInt(errorCode.httpCode); + sink.flush(); + } + + @Override public synchronized void data(boolean outFinished, int streamId, Buffer source) + throws IOException { + data(outFinished, streamId, source, (int) source.size()); + } + + @Override public synchronized void data(boolean outFinished, int streamId, Buffer source, + int byteCount) throws IOException { + if (closed) throw new IOException("closed"); + byte flags = FLAG_NONE; + if (outFinished) flags |= FLAG_END_STREAM; + dataFrame(streamId, flags, source, byteCount); + } + + void dataFrame(int streamId, byte flags, Buffer buffer, int byteCount) throws IOException { + byte type = TYPE_DATA; + frameHeader(streamId, byteCount, type, flags); + if (byteCount > 0) { + sink.write(buffer, byteCount); + } + } + + @Override public synchronized void settings(Settings settings) throws IOException { + if (closed) throw new IOException("closed"); + int length = settings.size() * 6; + byte type = TYPE_SETTINGS; + byte flags = FLAG_NONE; + int streamId = 0; + frameHeader(streamId, length, type, flags); + for (int i = 0; i < Settings.COUNT; i++) { + if (!settings.isSet(i)) continue; + int id = i; + if (id == 4) id = 3; // SETTINGS_MAX_CONCURRENT_STREAMS renumbered. + else if (id == 7) id = 4; // SETTINGS_INITIAL_WINDOW_SIZE renumbered. + sink.writeShort(id); + sink.writeInt(settings.get(i)); + } + sink.flush(); + } + + @Override public synchronized void ping(boolean ack, int payload1, int payload2) + throws IOException { + if (closed) throw new IOException("closed"); + int length = 8; + byte type = TYPE_PING; + byte flags = ack ? FLAG_ACK : FLAG_NONE; + int streamId = 0; + frameHeader(streamId, length, type, flags); + sink.writeInt(payload1); + sink.writeInt(payload2); + sink.flush(); + } + + @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, + byte[] debugData) throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.httpCode == -1) throw illegalArgument("errorCode.httpCode == -1"); + int length = 8 + debugData.length; + byte type = TYPE_GOAWAY; + byte flags = FLAG_NONE; + int streamId = 0; + frameHeader(streamId, length, type, flags); + sink.writeInt(lastGoodStreamId); + sink.writeInt(errorCode.httpCode); + if (debugData.length > 0) { + sink.write(debugData); + } + sink.flush(); + } + + @Override public synchronized void windowUpdate(int streamId, long windowSizeIncrement) + throws IOException { + if (closed) throw new IOException("closed"); + if (windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL) { + throw illegalArgument("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: %s", + windowSizeIncrement); + } + int length = 4; + byte type = TYPE_WINDOW_UPDATE; + byte flags = FLAG_NONE; + frameHeader(streamId, length, type, flags); + sink.writeInt((int) windowSizeIncrement); + sink.flush(); + } + + @Override public synchronized void close() throws IOException { + closed = true; + sink.close(); + } + + void frameHeader(int streamId, int length, byte type, byte flags) throws IOException { + if (logger.isLoggable(FINE)) logger.fine(formatHeader(false, streamId, length, type, flags)); + if (length > MAX_FRAME_SIZE) { + throw illegalArgument("FRAME_SIZE_ERROR length > %d: %d", MAX_FRAME_SIZE, length); + } + if ((streamId & 0x80000000) != 0) throw illegalArgument("reserved bit set: %s", streamId); + sink.writeInt((length & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); + sink.writeInt(streamId & 0x7fffffff); + } + } + + private static IllegalArgumentException illegalArgument(String message, Object... args) { + throw new IllegalArgumentException(format(message, args)); + } + + private static IOException ioException(String message, Object... args) throws IOException { + throw new IOException(format(message, args)); + } + + /** + * Decompression of the header block occurs above the framing layer. This + * class lazily reads continuation frames as they are needed by {@link + * HpackDraft08.Reader#readHeaders()}. + */ + static final class ContinuationSource implements Source { + private final BufferedSource source; + + short length; + byte flags; + int streamId; + + short left; + short padding; + + public ContinuationSource(BufferedSource source) { + this.source = source; + } + + @Override public long read(Buffer sink, long byteCount) throws IOException { + while (left == 0) { + source.skip(padding); + padding = 0; + if ((flags & FLAG_END_HEADERS) != 0) return -1; + readContinuationHeader(); + // TODO: test case for empty continuation header? + } + + long read = source.read(sink, Math.min(byteCount, left)); + if (read == -1) return -1; + left -= read; + return read; + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public void close() throws IOException { + } + + private void readContinuationHeader() throws IOException { + int previousStreamId = streamId; + int w1 = source.readInt(); + int w2 = source.readInt(); + length = left = (short) ((w1 & 0x3fff0000) >> 16); + byte type = (byte) ((w1 & 0xff00) >> 8); + flags = (byte) (w1 & 0xff); + if (logger.isLoggable(FINE)) logger.fine(formatHeader(true, streamId, length, type, flags)); + streamId = (w2 & 0x7fffffff); + if (type != TYPE_CONTINUATION) throw ioException("%s != TYPE_CONTINUATION", type); + if (streamId != previousStreamId) throw ioException("TYPE_CONTINUATION streamId changed"); + } + } + + private static short lengthWithoutPadding(short length, byte flags, short padding) + throws IOException { + if ((flags & FLAG_PADDED) != 0) length--; // Account for reading the padding length. + if (padding > length) { + throw ioException("PROTOCOL_ERROR padding %s > remaining length %s", padding, length); + } + return (short) (length - padding); + } + + /** + * Logs a human-readable representation of HTTP/2 frame headers. + * + *

The format is: + * + *

+   *   direction streamID length type flags
+   * 
+ * Where direction is {@code <<} for inbound and {@code >>} for outbound. + * + *

For example, the following would indicate a HEAD request sent from + * the client. + *

+   * {@code
+   *   << 0x0000000f    12 HEADERS       END_HEADERS|END_STREAM
+   * }
+   * 
+ */ + static final class FrameLogger { + + static String formatHeader(boolean inbound, int streamId, int length, byte type, byte flags) { + String formattedType = type < TYPES.length ? TYPES[type] : format("0x%02x", type); + String formattedFlags = formatFlags(type, flags); + return format("%s 0x%08x %5d %-13s %s", inbound ? "<<" : ">>", streamId, length, + formattedType, formattedFlags); + } + + /** + * Looks up valid string representing flags from the table. Invalid + * combinations are represented in binary. + */ + // Visible for testing. + static String formatFlags(byte type, byte flags) { + if (flags == 0) return ""; + switch (type) { // Special case types that have 0 or 1 flag. + case TYPE_SETTINGS: + case TYPE_PING: + return flags == FLAG_ACK ? "ACK" : BINARY[flags]; + case TYPE_PRIORITY: + case TYPE_RST_STREAM: + case TYPE_GOAWAY: + case TYPE_WINDOW_UPDATE: + return BINARY[flags]; + } + String result = flags < FLAGS.length ? FLAGS[flags] : BINARY[flags]; + // Special case types that have overlap flag values. + if (type == TYPE_PUSH_PROMISE && (flags & FLAG_END_PUSH_PROMISE) != 0) { + return result.replace("HEADERS", "PUSH_PROMISE"); // TODO: Avoid allocation. + } else if (type == TYPE_DATA && (flags & FLAG_COMPRESSED) != 0) { + return result.replace("PRIORITY", "COMPRESSED"); // TODO: Avoid allocation. + } + return result; + } + + /** Lookup table for valid frame types. */ + private static final String[] TYPES = new String[] { + "DATA", + "HEADERS", + "PRIORITY", + "RST_STREAM", + "SETTINGS", + "PUSH_PROMISE", + "PING", + "GOAWAY", + "WINDOW_UPDATE", + "CONTINUATION" + }; + + /** + * Lookup table for valid flags for DATA, HEADERS, CONTINUATION. Invalid + * combinations are represented in binary. + */ + private static final String[] FLAGS = new String[0x40]; // Highest bit flag is 0x20. + private static final String[] BINARY = new String[256]; + + static { + for (int i = 0; i < BINARY.length; i++) { + BINARY[i] = format("%8s", Integer.toBinaryString(i)).replace(' ', '0'); + } + + FLAGS[FLAG_NONE] = ""; + FLAGS[FLAG_END_STREAM] = "END_STREAM"; + FLAGS[FLAG_END_SEGMENT] = "END_SEGMENT"; + FLAGS[FLAG_END_STREAM | FLAG_END_SEGMENT] = "END_STREAM|END_SEGMENT"; + int[] prefixFlags = + new int[] {FLAG_END_STREAM, FLAG_END_SEGMENT, FLAG_END_SEGMENT | FLAG_END_STREAM}; + + FLAGS[FLAG_PADDED] = "PADDED"; + for (int prefixFlag : prefixFlags) { + FLAGS[prefixFlag | FLAG_PADDED] = FLAGS[prefixFlag] + "|PADDED"; + } + + FLAGS[FLAG_END_HEADERS] = "END_HEADERS"; // Same as END_PUSH_PROMISE. + FLAGS[FLAG_PRIORITY] = "PRIORITY"; // Same as FLAG_COMPRESSED. + FLAGS[FLAG_END_HEADERS | FLAG_PRIORITY] = "END_HEADERS|PRIORITY"; // Only valid on HEADERS. + int[] frameFlags = + new int[] {FLAG_END_HEADERS, FLAG_PRIORITY, FLAG_END_HEADERS | FLAG_PRIORITY}; + + for (int frameFlag : frameFlags) { + for (int prefixFlag : prefixFlags) { + FLAGS[prefixFlag | frameFlag] = FLAGS[prefixFlag] + '|' + FLAGS[frameFlag]; + FLAGS[prefixFlag | frameFlag | FLAG_PADDED] = + FLAGS[prefixFlag] + '|' + FLAGS[frameFlag] + "|PADDED"; + } + } + + for (int i = 0; i < FLAGS.length; i++) { // Fill in holes with binary representation. + if (FLAGS[i] == null) FLAGS[i] = BINARY[i]; + } + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Huffman.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Huffman.java new file mode 100644 index 000000000..dfa7153ef --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Huffman.java @@ -0,0 +1,225 @@ +/* + * Copyright 2013 Twitter, 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class was originally composed from the following classes in + * Twitter Hpack. + *
    + *
  • {@code com.twitter.hpack.HuffmanEncoder}
  • + *
  • {@code com.twitter.hpack.HuffmanDecoder}
  • + *
  • {@code com.twitter.hpack.HpackUtil}
  • + *
+ */ +class Huffman { + + // Appendix C: Huffman Codes + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#appendix-C + private static final int[] CODES = { + 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8, + 0xffffea, 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed, + 0xfffffee, 0xfffffef, 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4, + 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, 0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8, + 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9, 0x7fb, 0xfa, 0x16, 0x17, 0x18, + 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, 0x20, 0xffb, + 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, + 0x1ffc, 0x3ffc, 0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75, + 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76, 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe, + 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6, 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, + 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, 0x7fffdd, 0x7fffde, 0xffffeb, + 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, 0x7fffe3, + 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda, + 0x1fffdd, 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd, + 0x3fffde, 0xfffff0, 0x1fffdf, 0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0, + 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef, 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, + 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb, 0x7fff1, 0x3fffe7, + 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf, + 0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, + 0x7ffffe2, 0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, + 0x7ffffe4, 0x7ffffe5, 0xfffec, 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, + 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4, + 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, + 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee + }; + + private static final byte[] CODE_LENGTHS = { + 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 30, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, 5, + 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, + 6, 6, 5, 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, 20, 22, 20, 20, 22, 22, 22, 23, + 22, 23, 23, 23, 23, 23, 24, 23, 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, + 24, 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, 21, 21, 22, 21, 23, 22, + 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, + 26, 24, 25, 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, 20, 24, 20, 21, + 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, + 27, 27, 27, 27, 26 + }; + + private static final Huffman INSTANCE = new Huffman(); + + public static Huffman get() { + return INSTANCE; + } + + private final Node root = new Node(); + + private Huffman() { + buildTree(); + } + + void encode(byte[] data, OutputStream out) throws IOException { + long current = 0; + int n = 0; + + for (int i = 0; i < data.length; i++) { + int b = data[i] & 0xFF; + int code = CODES[b]; + int nbits = CODE_LENGTHS[b]; + + current <<= nbits; + current |= code; + n += nbits; + + while (n >= 8) { + n -= 8; + out.write(((int) (current >> n))); + } + } + + if (n > 0) { + current <<= (8 - n); + current |= (0xFF >>> n); + out.write((int) current); + } + } + + int encodedLength(byte[] bytes) { + long len = 0; + + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i] & 0xFF; + len += CODE_LENGTHS[b]; + } + + return (int) ((len + 7) >> 3); + } + + byte[] decode(byte[] buf) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Node node = root; + int current = 0; + int nbits = 0; + for (int i = 0; i < buf.length; i++) { + int b = buf[i] & 0xFF; + current = (current << 8) | b; + nbits += 8; + while (nbits >= 8) { + int c = (current >>> (nbits - 8)) & 0xFF; + node = node.children[c]; + if (node.children == null) { + // terminal node + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } else { + // non-terminal node + nbits -= 8; + } + } + } + + while (nbits > 0) { + int c = (current << (8 - nbits)) & 0xFF; + node = node.children[c]; + if (node.children != null || node.terminalBits > nbits) { + break; + } + baos.write(node.symbol); + nbits -= node.terminalBits; + node = root; + } + + return baos.toByteArray(); + } + + private void buildTree() { + for (int i = 0; i < CODE_LENGTHS.length; i++) { + addCode(i, CODES[i], CODE_LENGTHS[i]); + } + } + + private void addCode(int sym, int code, byte len) { + Node terminal = new Node(sym, len); + + Node current = root; + while (len > 8) { + len -= 8; + int i = ((code >>> len) & 0xFF); + if (current.children == null) { + throw new IllegalStateException("invalid dictionary: prefix not unique"); + } + if (current.children[i] == null) { + current.children[i] = new Node(); + } + current = current.children[i]; + } + + int shift = 8 - len; + int start = (code << shift) & 0xFF; + int end = 1 << shift; + for (int i = start; i < start + end; i++) { + current.children[i] = terminal; + } + } + + private static final class Node { + + // Null if terminal. + private final Node[] children; + + // Terminal nodes have a symbol. + private final int symbol; + + // Number of bits represented in the terminal node. + private final int terminalBits; + + /** Construct an internal node. */ + Node() { + this.children = new Node[256]; + this.symbol = 0; // Not read. + this.terminalBits = 0; // Not read. + } + + /** + * Construct a terminal node. + * + * @param symbol symbol the node represents + * @param bits length of Huffman code in bits + */ + Node(int symbol, int bits) { + this.children = null; + this.symbol = symbol; + int b = bits & 0x07; + this.terminalBits = b == 0 ? 8 : b; + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/IncomingStreamHandler.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/IncomingStreamHandler.java new file mode 100644 index 000000000..d36799f78 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/IncomingStreamHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import java.io.IOException; + +/** Listener to be notified when a connected peer creates a new stream. */ +public interface IncomingStreamHandler { + IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() { + @Override public void receive(SpdyStream stream) throws IOException { + stream.close(ErrorCode.REFUSED_STREAM); + } + }; + + /** + * Handle a new stream from this connection's peer. Implementations should + * respond by either {@link SpdyStream#reply replying to the stream} or + * {@link SpdyStream#close closing it}. This response does not need to be + * synchronous. + */ + void receive(SpdyStream stream) throws IOException; +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/NameValueBlockReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/NameValueBlockReader.java new file mode 100644 index 000000000..adc15f898 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/NameValueBlockReader.java @@ -0,0 +1,119 @@ +/* + * 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okio.ForwardingSource; +import com.koushikdutta.async.http.spdy.okio.InflaterSource; +import com.koushikdutta.async.http.spdy.okio.Okio; +import com.koushikdutta.async.http.spdy.okio.Source; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * 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 { + /** This source transforms compressed bytes into uncompressed bytes. */ + private final InflaterSource inflaterSource; + + /** + * How many compressed bytes must be read into inflaterSource before + * {@link #readNameValueBlock} returns. + */ + private int compressedLimit; + + /** This source holds inflated bytes. */ + private final BufferedSource source; + + public NameValueBlockReader(BufferedSource source) { + // 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. + Source throttleSource = new ForwardingSource(source) { + @Override public long read(Buffer sink, long byteCount) throws IOException { + if (compressedLimit == 0) return -1; // Out of data for the current block. + long read = super.read(sink, Math.min(byteCount, compressedLimit)); + if (read == -1) return -1; + compressedLimit -= read; + return read; + } + }; + + // 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; + } + }; + + this.inflaterSource = new InflaterSource(throttleSource, inflater); + this.source = Okio.buffer(inflaterSource); + } + + public List
readNameValueBlock(int length) throws IOException { + this.compressedLimit += length; + + int numberOfPairs = source.readInt(); + if (numberOfPairs < 0) throw new IOException("numberOfPairs < 0: " + numberOfPairs); + if (numberOfPairs > 1024) throw new IOException("numberOfPairs > 1024: " + numberOfPairs); + + List
entries = new ArrayList
(numberOfPairs); + for (int i = 0; i < numberOfPairs; i++) { + ByteString name = readByteString().toAsciiLowercase(); + ByteString values = readByteString(); + if (name.size() == 0) throw new IOException("name.size == 0"); + entries.add(new Header(name, values)); + } + + doneReading(); + return entries; + } + + private ByteString readByteString() throws IOException { + int length = source.readInt(); + return source.readByteString(length); + } + + private void doneReading() throws IOException { + // Move any outstanding unread bytes into the inflater. One side-effect of + // deflate compression is that sometimes there are bytes remaining in the + // stream after we've consumed all of the content. + if (compressedLimit > 0) { + inflaterSource.refill(); + if (compressedLimit != 0) throw new IOException("compressedLimit > 0: " + compressedLimit); + } + } + + public void close() throws IOException { + source.close(); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Ping.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Ping.java new file mode 100644 index 000000000..0a82b4308 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Ping.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * A locally-originated ping. + */ +public final class Ping { + private final CountDownLatch latch = new CountDownLatch(1); + private long sent = -1; + private long received = -1; + + Ping() { + } + + public void send() { + if (sent != -1) throw new IllegalStateException(); + sent = System.nanoTime(); + } + + public void receive() { + if (received != -1 || sent == -1) throw new IllegalStateException(); + received = System.nanoTime(); + latch.countDown(); + } + + void cancel() { + if (received != -1 || sent == -1) throw new IllegalStateException(); + received = sent - 1; + latch.countDown(); + } + + /** + * Returns the round trip time for this ping in nanoseconds, waiting for the + * response to arrive if necessary. Returns -1 if the response was + * canceled. + */ + public long roundTripTime() throws InterruptedException { + latch.await(); + return received - sent; + } + + /** + * Returns the round trip time for this ping in nanoseconds, or -1 if the + * response was canceled, or -2 if the timeout elapsed before the round + * trip completed. + */ + public long roundTripTime(long timeout, TimeUnit unit) throws InterruptedException { + if (latch.await(timeout, unit)) { + return received - sent; + } else { + return -2; + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/PushObserver.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/PushObserver.java new file mode 100644 index 000000000..fcec1732b --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/PushObserver.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.http.spdy.okio.BufferedSource; + +import java.io.IOException; +import java.util.List; + +/** + * {@link com.squareup.okhttp.Protocol#HTTP_2 HTTP/2} only. + * Processes server-initiated HTTP requests on the client. Implementations must + * quickly dispatch callbacks to avoid creating a bottleneck. + * + *

While {@link #onReset} may occur at any time, the following callbacks are + * expected in order, correlated by stream ID. + *

    + *
  • {@link #onRequest}
  • + *
  • {@link #onHeaders} (unless canceled)
  • + *
  • {@link #onData} (optional sequence of data frames)
  • + *
+ * + *

As a stream ID is scoped to a single HTTP/2 connection, implementations + * which target multiple connections should expect repetition of stream IDs. + * + *

Return true to request cancellation of a pushed stream. Note that this + * does not guarantee future frames won't arrive on the stream ID. + */ +public interface PushObserver { + /** + * Describes the request that the server intends to push a response for. + * + * @param streamId server-initiated stream ID: an even number. + * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, + * {@code :authority}, and (@code :path}. + */ + boolean onRequest(int streamId, List

requestHeaders); + + /** + * The response headers corresponding to a pushed request. When {@code last} + * is true, there are no data frames to follow. + * + * @param streamId server-initiated stream ID: an even number. + * @param responseHeaders minimally includes {@code :status}. + * @param last when true, there is no response data. + */ + boolean onHeaders(int streamId, List
responseHeaders, boolean last); + + /** + * A chunk of response data corresponding to a pushed request. This data + * must either be read or skipped. + * + * @param streamId server-initiated stream ID: an even number. + * @param source location of data corresponding with this stream ID. + * @param byteCount number of bytes to read or skip from the source. + * @param last when true, there are no data frames to follow. + */ + boolean onData(int streamId, BufferedSource source, int byteCount, boolean last) + throws IOException; + + /** Indicates the reason why this stream was canceled. */ + void onReset(int streamId, ErrorCode errorCode); + + PushObserver CANCEL = new PushObserver() { + + @Override public boolean onRequest(int streamId, List
requestHeaders) { + return true; + } + + @Override public boolean onHeaders(int streamId, List
responseHeaders, boolean last) { + return true; + } + + @Override public boolean onData(int streamId, BufferedSource source, int byteCount, + boolean last) throws IOException { + source.skip(byteCount); + return true; + } + + @Override public void onReset(int streamId, ErrorCode errorCode) { + } + }; +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java new file mode 100644 index 000000000..4b332f6c9 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2012 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import java.util.Arrays; + +/** + * Settings describe characteristics of the sending peer, which are used by the receiving peer. + * Settings are {@link com.koushikdutta.async.http.spdy.okhttp.internal.spdy.SpdyConnection connection} scoped. + */ +public final class Settings { + /** + * From the SPDY/3 and HTTP/2 specs, the default initial window size for all + * streams is 64 KiB. (Chrome 25 uses 10 MiB). + */ + public static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024; + + /** Peer request to clear durable settings. */ + static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1; + + /** Sent by servers only. The peer requests this setting persisted for future connections. */ + static final int PERSIST_VALUE = 0x1; + /** Sent by clients only. The client is reminding the server of a persisted value. */ + static final int PERSISTED = 0x2; + + /** spdy/3: Sender's estimate of max incoming kbps. */ + static final int UPLOAD_BANDWIDTH = 1; + /** HTTP/2: Size in bytes of the table used to decode the sender's header blocks. */ + static final int HEADER_TABLE_SIZE = 1; + /** spdy/3: Sender's estimate of max outgoing kbps. */ + static final int DOWNLOAD_BANDWIDTH = 2; + /** HTTP/2: The peer must not send a PUSH_PROMISE frame when this is 0. */ + static final int ENABLE_PUSH = 2; + /** spdy/3: Sender's estimate of millis between sending a request and receiving a response. */ + static final int ROUND_TRIP_TIME = 3; + /** Sender's maximum number of concurrent streams. */ + static final int MAX_CONCURRENT_STREAMS = 4; + /** spdy/3: Current CWND in Packets. */ + static final int CURRENT_CWND = 5; + /** spdy/3: Retransmission rate. Percentage */ + static final int DOWNLOAD_RETRANS_RATE = 6; + /** Window size in bytes. */ + static final int INITIAL_WINDOW_SIZE = 7; + /** spdy/3: Window size in bytes. */ + static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 8; + /** Flow control options. */ + static final int FLOW_CONTROL_OPTIONS = 10; + + /** Total number of settings. */ + 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; + + /** Bitfield of flags that have {@link #PERSIST_VALUE}. */ + private int persistValue; + + /** Bitfield of flags that have {@link #PERSISTED}. */ + private int persisted; + + /** Flag values. */ + private final int[] values = new int[COUNT]; + + public void clear() { + set = persistValue = persisted = 0; + Arrays.fill(values, 0); + } + + Settings set(int id, int idFlags, int value) { + if (id >= values.length) { + return this; // Discard unknown settings. + } + + int bit = 1 << id; + set |= bit; + if ((idFlags & PERSIST_VALUE) != 0) { + persistValue |= bit; + } else { + persistValue &= ~bit; + } + if ((idFlags & PERSISTED) != 0) { + persisted |= bit; + } else { + persisted &= ~bit; + } + + values[id] = value; + return this; + } + + /** Returns true if a value has been assigned for the setting {@code id}. */ + boolean isSet(int id) { + int bit = 1 << id; + return (set & bit) != 0; + } + + /** Returns the value for the setting {@code id}, or 0 if unset. */ + int get(int id) { + return values[id]; + } + + /** Returns the flags for the setting {@code id}, or 0 if unset. */ + int flags(int id) { + int result = 0; + if (isPersisted(id)) result |= Settings.PERSISTED; + if (persistValue(id)) result |= Settings.PERSIST_VALUE; + return result; + } + + /** Returns the number of settings that have values assigned. */ + int size() { + return Integer.bitCount(set); + } + + /** spdy/3 only. */ + int getUploadBandwidth(int defaultValue) { + int bit = 1 << UPLOAD_BANDWIDTH; + return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue; + } + + /** HTTP/2 only. Returns -1 if unset. */ + int getHeaderTableSize() { + int bit = 1 << HEADER_TABLE_SIZE; + return (bit & set) != 0 ? values[HEADER_TABLE_SIZE] : -1; + } + + /** spdy/3 only. */ + int getDownloadBandwidth(int defaultValue) { + int bit = 1 << DOWNLOAD_BANDWIDTH; + return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue; + } + + /** HTTP/2 only. */ + // TODO: honor this setting in HTTP/2. + boolean getEnablePush(boolean defaultValue) { + int bit = 1 << ENABLE_PUSH; + return ((bit & set) != 0 ? values[ENABLE_PUSH] : defaultValue ? 1 : 0) == 1; + } + + /** spdy/3 only. */ + int getRoundTripTime(int defaultValue) { + int bit = 1 << ROUND_TRIP_TIME; + return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue; + } + + // TODO: honor this setting in spdy/3 and HTTP/2. + int getMaxConcurrentStreams(int defaultValue) { + int bit = 1 << MAX_CONCURRENT_STREAMS; + return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue; + } + + /** spdy/3 only. */ + int getCurrentCwnd(int defaultValue) { + int bit = 1 << CURRENT_CWND; + return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue; + } + + /** spdy/3 only. */ + int getDownloadRetransRate(int defaultValue) { + int bit = 1 << DOWNLOAD_RETRANS_RATE; + return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue; + } + + public int getInitialWindowSize(int defaultValue) { + int bit = 1 << INITIAL_WINDOW_SIZE; + return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue; + } + + /** spdy/3 only. */ + int getClientCertificateVectorSize(int defaultValue) { + int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE; + return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue; + } + + // TODO: honor this setting in spdy/3 and HTTP/2. + 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/3 + * connections to the same host. + */ + boolean persistValue(int id) { + int bit = 1 << id; + return (persistValue & bit) != 0; + } + + /** Returns true if this setting was persisted. */ + boolean isPersisted(int id) { + int bit = 1 << id; + return (persisted & bit) != 0; + } + + /** + * Writes {@code other} into this. If any setting is populated by this and + * {@code other}, the value and flags from {@code other} will be kept. + */ + public void merge(Settings other) { + for (int i = 0; i < COUNT; i++) { + if (!other.isSet(i)) continue; + set(i, other.flags(i), other.get(i)); + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java new file mode 100644 index 000000000..72a3df2ba --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java @@ -0,0 +1,514 @@ +/* + * 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.http.Protocol; +import com.koushikdutta.async.http.spdy.okhttp.internal.Util; +import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.http.spdy.okio.BufferedSink; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okio.DeflaterSink; +import com.koushikdutta.async.http.spdy.okio.Okio; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.ProtocolException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.List; +import java.util.zip.Deflater; + + +/** + * Read and write spdy/3.1 frames. + * http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1 + */ +public final class Spdy3 implements Variant { + + @Override public Protocol getProtocol() { + return Protocol.SPDY_3; + } + + 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_PING = 0x6; + static final int TYPE_GOAWAY = 0x7; + static final int TYPE_HEADERS = 0x8; + static final int TYPE_WINDOW_UPDATE = 0x9; + + 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(BufferedSource source, boolean client) { + return new Reader(source, client); + } + + @Override public FrameWriter newWriter(BufferedSink sink, boolean client) { + return new Writer(sink, client); + } + + @Override public int maxFrameSize() { + return 16383; + } + + /** Read spdy/3 frames. */ + static final class Reader implements FrameReader { + private final BufferedSource source; + private final boolean client; + private final NameValueBlockReader headerBlockReader; + + Reader(BufferedSource source, boolean client) { + this.source = source; + this.headerBlockReader = new NameValueBlockReader(this.source); + this.client = client; + } + + @Override public void readConnectionPreface() { + } + + @Override + public boolean canProcessFrame(ByteBufferList bb) { + if (bb.remaining() < 8) + return false; + ByteBuffer peek = ByteBuffer.wrap(bb.peekBytes(8)).order(ByteOrder.BIG_ENDIAN); + peek.getInt(); + int w2 = peek.getInt(); + + int length = (w2 & 0xffffff); + return bb.remaining() >= 8 + length; + } + + /** + * 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; + int w2; + try { + w1 = source.readInt(); + w2 = source.readInt(); + } catch (IOException e) { + return false; // This might be a normal socket close. + } + + 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_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; + + default: + source.skip(length); + return true; + } + } else { + int streamId = w1 & 0x7fffffff; + boolean inFinished = (flags & FLAG_FIN) != 0; + handler.data(inFinished, streamId, source, length); + return true; + } + } + + private void readSynStream(Handler handler, int flags, int length) throws IOException { + int w1 = source.readInt(); + int w2 = source.readInt(); + int streamId = w1 & 0x7fffffff; + int associatedStreamId = w2 & 0x7fffffff; + source.readShort(); // int priority = (s3 & 0xe000) >>> 13; int slot = s3 & 0xff; + List
headerBlock = headerBlockReader.readNameValueBlock(length - 10); + + boolean inFinished = (flags & FLAG_FIN) != 0; + boolean outFinished = (flags & FLAG_UNIDIRECTIONAL) != 0; + handler.headers(outFinished, inFinished, streamId, associatedStreamId, headerBlock, + HeadersMode.SPDY_SYN_STREAM); + } + + private void readSynReply(Handler handler, int flags, int length) throws IOException { + int w1 = source.readInt(); + int streamId = w1 & 0x7fffffff; + List
headerBlock = headerBlockReader.readNameValueBlock(length - 4); + boolean inFinished = (flags & FLAG_FIN) != 0; + handler.headers(false, inFinished, streamId, -1, headerBlock, 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 = source.readInt() & 0x7fffffff; + int errorCodeInt = source.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 = source.readInt(); + int streamId = w1 & 0x7fffffff; + List
headerBlock = headerBlockReader.readNameValueBlock(length - 4); + handler.headers(false, false, streamId, -1, headerBlock, 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 = source.readInt(); + int w2 = source.readInt(); + int streamId = w1 & 0x7fffffff; + long increment = w2 & 0x7fffffff; + if (increment == 0) throw ioException("windowSizeIncrement was 0", increment); + handler.windowUpdate(streamId, increment); + } + + private void readPing(Handler handler, int flags, int length) throws IOException { + if (length != 4) throw ioException("TYPE_PING length: %d != 4", length); + int id = source.readInt(); + boolean ack = client == ((id & 1) == 1); + handler.ping(ack, 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 = source.readInt() & 0x7fffffff; + int errorCodeInt = source.readInt(); + ErrorCode errorCode = ErrorCode.fromSpdyGoAway(errorCodeInt); + if (errorCode == null) { + throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt); + } + handler.goAway(lastGoodStreamId, errorCode, ByteString.EMPTY); + } + + private void readSettings(Handler handler, int flags, int length) throws IOException { + int numberOfEntries = source.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 = source.readInt(); + int value = source.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 { + headerBlockReader.close(); + } + } + + /** Write spdy/3 frames. */ + static final class Writer implements FrameWriter { + private final BufferedSink sink; + private final Buffer headerBlockBuffer; + private final BufferedSink headerBlockOut; + private final boolean client; + private boolean closed; + + Writer(BufferedSink sink, boolean client) { + this.sink = sink; + this.client = client; + + Deflater deflater = new Deflater(); + deflater.setDictionary(DICTIONARY); + headerBlockBuffer = new Buffer(); + headerBlockOut = Okio.buffer(new DeflaterSink(headerBlockBuffer, deflater)); + } + + @Override public void ackSettings() { + // Do nothing: no ACK for SPDY/3 settings. + } + + @Override + public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) + throws IOException { + // Do nothing: no push promise for SPDY/3. + } + + @Override public synchronized void connectionPreface() { + // Do nothing: no connection preface for SPDY/3. + } + + @Override public synchronized void flush() throws IOException { + if (closed) throw new IOException("closed"); + sink.flush(); + } + + @Override public synchronized void synStream(boolean outFinished, boolean inFinished, + int streamId, int associatedStreamId, List
headerBlock) + throws IOException { + if (closed) throw new IOException("closed"); + writeNameValueBlockToBuffer(headerBlock); + int length = (int) (10 + headerBlockBuffer.size()); + int type = TYPE_SYN_STREAM; + int flags = (outFinished ? FLAG_FIN : 0) | (inFinished ? FLAG_UNIDIRECTIONAL : 0); + + int unused = 0; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeInt(associatedStreamId & 0x7fffffff); + sink.writeShort((unused & 0x7) << 13 | (unused & 0x1f) << 8 | (unused & 0xff)); + sink.writeAll(headerBlockBuffer); + sink.flush(); + } + + @Override public synchronized void synReply(boolean outFinished, int streamId, + List
headerBlock) throws IOException { + if (closed) throw new IOException("closed"); + writeNameValueBlockToBuffer(headerBlock); + int type = TYPE_SYN_REPLY; + int flags = (outFinished ? FLAG_FIN : 0); + int length = (int) (headerBlockBuffer.size() + 4); + + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeAll(headerBlockBuffer); + sink.flush(); + } + + @Override public synchronized void headers(int streamId, List
headerBlock) + throws IOException { + if (closed) throw new IOException("closed"); + writeNameValueBlockToBuffer(headerBlock); + int flags = 0; + int type = TYPE_HEADERS; + int length = (int) (headerBlockBuffer.size() + 4); + + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeAll(headerBlockBuffer); + } + + @Override public synchronized void rstStream(int streamId, ErrorCode errorCode) + throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException(); + int flags = 0; + int type = TYPE_RST_STREAM; + int length = 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeInt(errorCode.spdyRstCode); + sink.flush(); + } + + @Override public synchronized void data(boolean outFinished, int streamId, Buffer source) + throws IOException { + data(outFinished, streamId, source, (int) source.size()); + } + + @Override public synchronized void data(boolean outFinished, int streamId, Buffer source, + int byteCount) throws IOException { + int flags = (outFinished ? FLAG_FIN : 0); + sendDataFrame(streamId, flags, source, byteCount); + } + + void sendDataFrame(int streamId, int flags, Buffer buffer, int byteCount) + throws IOException { + if (closed) throw new IOException("closed"); + if (byteCount > 0xffffffL) { + throw new IllegalArgumentException("FRAME_TOO_LARGE max size is 16Mib: " + byteCount); + } + sink.writeInt(streamId & 0x7fffffff); + sink.writeInt((flags & 0xff) << 24 | byteCount & 0xffffff); + if (byteCount > 0) { + sink.write(buffer, byteCount); + } + } + + private void writeNameValueBlockToBuffer(List
headerBlock) throws IOException { + if (headerBlockBuffer.size() != 0) throw new IllegalStateException(); + headerBlockOut.writeInt(headerBlock.size()); + for (int i = 0, size = headerBlock.size(); i < size; i++) { + ByteString name = headerBlock.get(i).name; + headerBlockOut.writeInt(name.size()); + headerBlockOut.write(name); + ByteString value = headerBlock.get(i).value; + headerBlockOut.writeInt(value.size()); + headerBlockOut.write(value); + } + headerBlockOut.flush(); + } + + @Override public synchronized void settings(Settings settings) throws IOException { + if (closed) throw new IOException("closed"); + int type = TYPE_SETTINGS; + int flags = 0; + int size = settings.size(); + int length = 4 + size * 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(size); + for (int i = 0; i <= Settings.COUNT; i++) { + if (!settings.isSet(i)) continue; + int settingsFlags = settings.flags(i); + sink.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff)); + sink.writeInt(settings.get(i)); + } + sink.flush(); + } + + @Override public synchronized void ping(boolean reply, int payload1, int payload2) + throws IOException { + if (closed) throw new IOException("closed"); + boolean payloadIsReply = client != ((payload1 & 1) == 1); + if (reply != payloadIsReply) throw new IllegalArgumentException("payload != reply"); + int type = TYPE_PING; + int flags = 0; + int length = 4; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(payload1); + sink.flush(); + } + + @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, + byte[] ignored) throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.spdyGoAwayCode == -1) { + throw new IllegalArgumentException("errorCode.spdyGoAwayCode == -1"); + } + int type = TYPE_GOAWAY; + int flags = 0; + int length = 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(lastGoodStreamId); + sink.writeInt(errorCode.spdyGoAwayCode); + sink.flush(); + } + + @Override public synchronized void windowUpdate(int streamId, long increment) + throws IOException { + if (closed) throw new IOException("closed"); + if (increment == 0 || increment > 0x7fffffffL) { + throw new IllegalArgumentException( + "windowSizeIncrement must be between 1 and 0x7fffffff: " + increment); + } + int type = TYPE_WINDOW_UPDATE; + int flags = 0; + int length = 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId); + sink.writeInt((int) increment); + sink.flush(); + } + + @Override public synchronized void close() throws IOException { + closed = true; + Util.closeAll(sink, headerBlockOut); + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyConnection.java new file mode 100644 index 000000000..52f924f14 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyConnection.java @@ -0,0 +1,874 @@ +/* + * 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.http.Protocol; +import com.koushikdutta.async.http.spdy.okhttp.internal.NamedRunnable; +import com.koushikdutta.async.http.spdy.okhttp.internal.Util; +import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okio.Okio; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + + +import static com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE; + +/** + * A socket connection to a remote peer. A connection hosts streams which can + * send and receive data. + * + *

Many methods in this API are synchronous: the call is + * completed before the method returns. This is typical for Java but atypical + * for SPDY. This is motivated by exception transparency: an IOException that + * was triggered by a certain caller can be caught and handled by that caller. + */ +public final class SpdyConnection implements Closeable { + + // Internal state of this connection is guarded by 'this'. No blocking + // operations may be performed while holding this lock! + // + // Socket writes are guarded by frameWriter. + // + // Socket reads are unguarded but are only made by the reader thread. + // + // Certain operations (like SYN_STREAM) need to synchronize on both the + // 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'. + + private static final ExecutorService executor = new ThreadPoolExecutor(0, + Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue(), + Util.threadFactory("OkHttp SpdyConnection", true)); + + /** The protocol variant, like {@link com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3}. */ + final Protocol protocol; + + /** True if this peer initiated the connection. */ + final boolean client; + + /** + * User code to run in response to an incoming stream. Callbacks must not be + * run on the callback executor. + */ + private final IncomingStreamHandler handler; + private final Map streams = new HashMap(); + private final String hostName; + private int lastGoodStreamId; + private int nextStreamId; + private boolean shutdown; + private long idleStartTimeNs = System.nanoTime(); + + /** Ensures push promise callbacks events are sent in order per stream. */ + private final ExecutorService pushExecutor; + + /** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */ + private Map pings; + /** User code to run in response to push promise events. */ + private final PushObserver pushObserver; + private int nextPingId; + + /** + * The total number of bytes consumed by the application, but not yet + * acknowledged by sending a {@code WINDOW_UPDATE} frame on this connection. + */ + // Visible for testing + long unacknowledgedBytesRead = 0; + + /** + * Count of bytes that can be written on the connection before receiving a + * window update. + */ + // Visible for testing + long bytesLeftInWriteWindow; + + /** Settings we communicate to the peer. */ + // TODO: Do we want to dynamically adjust settings, or KISS and only set once? + final Settings okHttpSettings = new Settings(); + // okHttpSettings.set(Settings.MAX_CONCURRENT_STREAMS, 0, max); + private static final int OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024; + + /** Settings we receive from the peer. */ + // TODO: MWS will need to guard on this setting before attempting to push. + final Settings peerSettings = new Settings(); + + private boolean receivedInitialPeerSettings = false; + final Variant variant; + final Socket socket; + final FrameWriter frameWriter; + final long maxFrameSize; + + // Visible for testing + final Reader readerRunnable; + + private SpdyConnection(Builder builder) throws IOException { + protocol = builder.protocol; + pushObserver = builder.pushObserver; + client = builder.client; + handler = builder.handler; + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-13#section-5.1.1 + nextStreamId = builder.client ? 1 : 2; + if (builder.client && protocol == Protocol.HTTP_2) { + nextStreamId += 2; // In HTTP/2, 1 on client is reserved for Upgrade. + } + + nextPingId = builder.client ? 1 : 2; + + // Flow control was designed more for servers, or proxies than edge clients. + // If we are a client, set the flow control window to 16MiB. This avoids + // thrashing window updates every 64KiB, yet small enough to avoid blowing + // up the heap. + if (builder.client) { + okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, OKHTTP_CLIENT_WINDOW_SIZE); + } + + hostName = builder.hostName; + + if (protocol == Protocol.HTTP_2) { + variant = new Http20Draft13(); + // Like newSingleThreadExecutor, except lazy creates the thread. + pushExecutor = new ThreadPoolExecutor(0, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + Util.threadFactory(String.format("OkHttp %s Push Observer", hostName), true)); + // 1 less than SPDY http://tools.ietf.org/html/draft-ietf-httpbis-http2-13#section-6.9.2 + peerSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, 65535); + } else if (protocol == Protocol.SPDY_3) { + variant = new Spdy3(); + pushExecutor = null; + } else { + throw new AssertionError(protocol); + } + bytesLeftInWriteWindow = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); + socket = builder.socket; + frameWriter = variant.newWriter(Okio.buffer(Okio.sink(builder.socket)), client); + maxFrameSize = variant.maxFrameSize(); + + readerRunnable = new Reader(); + new Thread(readerRunnable).start(); // Not a daemon thread. + } + + /** The protocol as selected using NPN or ALPN. */ + public Protocol getProtocol() { + return protocol; + } + + /** + * Returns the number of {@link SpdyStream#isOpen() open streams} on this + * connection. + */ + public synchronized int openStreamCount() { + return streams.size(); + } + + synchronized SpdyStream getStream(int id) { + return streams.get(id); + } + + synchronized SpdyStream removeStream(int streamId) { + SpdyStream stream = streams.remove(streamId); + if (stream != null && streams.isEmpty()) { + setIdle(true); + } + return stream; + } + + private synchronized void setIdle(boolean value) { + idleStartTimeNs = value ? System.nanoTime() : Long.MAX_VALUE; + } + + /** Returns true if this connection is idle. */ + public synchronized boolean isIdle() { + return idleStartTimeNs != Long.MAX_VALUE; + } + + /** + * 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; + } + + /** + * Returns a new server-initiated stream. + * + * @param associatedStreamId the stream that triggered the sender to create + * this stream. + * @param out true to create an output stream that we can use to send data + * to the remote peer. Corresponds to {@code FLAG_FIN}. + */ + public SpdyStream pushStream(int associatedStreamId, List

requestHeaders, boolean out) + throws IOException { + if (client) throw new IllegalStateException("Client cannot push requests."); + if (protocol != Protocol.HTTP_2) throw new IllegalStateException("protocol != HTTP_2"); + return newStream(associatedStreamId, requestHeaders, out, false); + } + + /** + * Returns a new locally-initiated stream. + * + * @param out true to create an output stream that we can use to send data to the remote peer. + * Corresponds to {@code FLAG_FIN}. + * @param in true to create an input stream that the remote peer can use to send data to us. + * Corresponds to {@code FLAG_UNIDIRECTIONAL}. + */ + public SpdyStream newStream(List
requestHeaders, boolean out, boolean in) + throws IOException { + return newStream(0, requestHeaders, out, in); + } + + private SpdyStream newStream(int associatedStreamId, List
requestHeaders, boolean out, + boolean in) throws IOException { + boolean outFinished = !out; + boolean inFinished = !in; + SpdyStream stream; + int streamId; + + synchronized (frameWriter) { + synchronized (this) { + if (shutdown) { + throw new IOException("shutdown"); + } + streamId = nextStreamId; + nextStreamId += 2; + stream = new SpdyStream(streamId, this, outFinished, inFinished, requestHeaders); + if (stream.isOpen()) { + streams.put(streamId, stream); + setIdle(false); + } + } + if (associatedStreamId == 0) { + frameWriter.synStream(outFinished, inFinished, streamId, associatedStreamId, + requestHeaders); + } else if (client) { + throw new IllegalArgumentException("client streams shouldn't have associated stream IDs"); + } else { // HTTP/2 has a PUSH_PROMISE frame. + frameWriter.pushPromise(associatedStreamId, streamId, requestHeaders); + } + } + + if (!out) { + frameWriter.flush(); + } + + return stream; + } + + void writeSynReply(int streamId, boolean outFinished, List
alternating) + throws IOException { + frameWriter.synReply(outFinished, streamId, alternating); + } + + /** + * Callers of this method are not thread safe, and sometimes on application + * threads. Most often, this method will be called to send a buffer worth of + * data to the peer. + *

+ * Writes are subject to the write window of the stream and the connection. + * Until there is a window sufficient to send {@code byteCount}, the caller + * will block. For example, a user of {@code HttpURLConnection} who flushes + * more bytes to the output stream than the connection's write window will + * block. + *

+ * Zero {@code byteCount} writes are not subject to flow control and + * will not block. The only use case for zero {@code byteCount} is closing + * a flushed output stream. + */ + public void writeData(int streamId, boolean outFinished, Buffer buffer, long byteCount) + throws IOException { + if (byteCount == 0) { // Empty data frames are not flow-controlled. + frameWriter.data(outFinished, streamId, buffer, 0); + return; + } + + while (byteCount > 0) { + int toWrite; + synchronized (SpdyConnection.this) { + try { + while (bytesLeftInWriteWindow <= 0) { + SpdyConnection.this.wait(); // Wait until we receive a WINDOW_UPDATE. + } + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + + toWrite = (int) Math.min(Math.min(byteCount, bytesLeftInWriteWindow), maxFrameSize); + bytesLeftInWriteWindow -= toWrite; + } + + byteCount -= toWrite; + frameWriter.data(outFinished && byteCount == 0, streamId, buffer, toWrite); + } + } + + /** + * {@code delta} will be negative if a settings frame initial window is + * smaller than the last. + */ + void addBytesToWriteWindow(long delta) { + bytesLeftInWriteWindow += delta; + if (delta > 0) SpdyConnection.this.notifyAll(); + } + + void writeSynResetLater(final int streamId, final ErrorCode errorCode) { + executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) { + @Override public void execute() { + try { + writeSynReset(streamId, errorCode); + } catch (IOException ignored) { + } + } + }); + } + + void writeSynReset(int streamId, ErrorCode statusCode) throws IOException { + frameWriter.rstStream(streamId, statusCode); + } + + void writeWindowUpdateLater(final int streamId, final long unacknowledgedBytesRead) { + executor.submit(new NamedRunnable("OkHttp Window Update %s stream %d", hostName, streamId) { + @Override public void execute() { + try { + frameWriter.windowUpdate(streamId, unacknowledgedBytesRead); + } catch (IOException ignored) { + } + } + }); + } + + /** + * Sends a ping frame to the peer. Use the returned object to await the + * ping's response and observe its round trip time. + */ + public Ping ping() throws IOException { + Ping ping = new Ping(); + int pingId; + synchronized (this) { + if (shutdown) { + throw new IOException("shutdown"); + } + pingId = nextPingId; + nextPingId += 2; + if (pings == null) pings = new HashMap(); + pings.put(pingId, ping); + } + writePing(false, pingId, 0x4f4b6f6b /* ASCII "OKok" */, ping); + return ping; + } + + private void writePingLater( + final boolean reply, final int payload1, final int payload2, final Ping ping) { + executor.submit(new NamedRunnable("OkHttp %s ping %08x%08x", + hostName, payload1, payload2) { + @Override public void execute() { + try { + writePing(reply, payload1, payload2, ping); + } catch (IOException ignored) { + } + } + }); + } + + 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(); + frameWriter.ping(reply, payload1, payload2); + } + } + + private synchronized Ping removePing(int id) { + return pings != null ? pings.remove(id) : null; + } + + public void flush() throws IOException { + frameWriter.flush(); + } + + /** + * Degrades this connection such that new streams can neither be created + * locally, nor accepted from the remote peer. Existing streams are not + * impacted. This is intended to permit an endpoint to gracefully stop + * accepting new requests without harming previously established streams. + */ + public void shutdown(ErrorCode statusCode) throws IOException { + synchronized (frameWriter) { + int lastGoodStreamId; + synchronized (this) { + if (shutdown) { + return; + } + shutdown = true; + lastGoodStreamId = this.lastGoodStreamId; + } + // TODO: propagate exception message into debugData + frameWriter.goAway(lastGoodStreamId, statusCode, Util.EMPTY_BYTE_ARRAY); + } + } + + /** + * Closes this connection. This cancels all open streams and unanswered + * pings. It closes the underlying input and output streams and shuts down + * internal executor services. + */ + @Override public void close() throws IOException { + close(ErrorCode.NO_ERROR, ErrorCode.CANCEL); + } + + private void close(ErrorCode connectionCode, ErrorCode streamCode) throws IOException { + assert (!Thread.holdsLock(this)); + IOException thrown = null; + try { + shutdown(connectionCode); + } catch (IOException e) { + thrown = e; + } + + SpdyStream[] streamsToClose = null; + Ping[] pingsToCancel = null; + synchronized (this) { + if (!streams.isEmpty()) { + streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]); + streams.clear(); + setIdle(false); + } + if (pings != null) { + pingsToCancel = pings.values().toArray(new Ping[pings.size()]); + pings = null; + } + } + + if (streamsToClose != null) { + for (SpdyStream stream : streamsToClose) { + try { + stream.close(streamCode); + } catch (IOException e) { + if (thrown != null) thrown = e; + } + } + } + + if (pingsToCancel != null) { + for (Ping ping : pingsToCancel) { + ping.cancel(); + } + } + + // Close the writer to release its resources (such as deflaters). + try { + frameWriter.close(); + } catch (IOException e) { + if (thrown == null) thrown = e; + } + + // Close the socket to break out the reader thread, which will clean up after itself. + try { + socket.close(); + } catch (IOException e) { + thrown = e; + } + + 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 sendConnectionPreface() throws IOException { + frameWriter.connectionPreface(); + frameWriter.settings(okHttpSettings); + int windowSize = okHttpSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); + if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) { + frameWriter.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE); + } + } + + public static class Builder { + private String hostName; + private Socket socket; + private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS; + private Protocol protocol = Protocol.SPDY_3; + private PushObserver pushObserver = PushObserver.CANCEL; + private boolean client; + + public Builder(boolean client, Socket socket) throws IOException { + this(((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(), client, socket); + } + + /** + * @param client true if this peer initiated the connection; false if this + * peer accepted the connection. + */ + public Builder(String hostName, boolean client, Socket socket) throws IOException { + this.hostName = hostName; + this.client = client; + this.socket = socket; + } + + public Builder handler(IncomingStreamHandler handler) { + this.handler = handler; + return this; + } + + public Builder protocol(Protocol protocol) { + this.protocol = protocol; + return this; + } + + public Builder pushObserver(PushObserver pushObserver) { + this.pushObserver = pushObserver; + return this; + } + + public SpdyConnection build() throws IOException { + return new SpdyConnection(this); + } + } + + /** + * Methods in this class must not lock FrameWriter. If a method needs to + * write a frame, create an async task to do so. + */ + class Reader extends NamedRunnable implements FrameReader.Handler { + FrameReader frameReader; + + private Reader() { + super("OkHttp %s", hostName); + } + + @Override protected void execute() { + ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR; + ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR; + try { + frameReader = variant.newReader(Okio.buffer(Okio.source(socket)), client); + if (!client) { + frameReader.readConnectionPreface(); + } + while (frameReader.nextFrame(this)) { + } + connectionErrorCode = ErrorCode.NO_ERROR; + streamErrorCode = ErrorCode.CANCEL; + } catch (IOException e) { + connectionErrorCode = ErrorCode.PROTOCOL_ERROR; + streamErrorCode = ErrorCode.PROTOCOL_ERROR; + } finally { + try { + close(connectionErrorCode, streamErrorCode); + } catch (IOException ignored) { + } + Util.closeQuietly(frameReader); + } + } + + @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length) + throws IOException { + if (pushedStream(streamId)) { + pushDataLater(streamId, source, length, inFinished); + return; + } + SpdyStream dataStream = getStream(streamId); + if (dataStream == null) { + writeSynResetLater(streamId, ErrorCode.INVALID_STREAM); + source.skip(length); + return; + } + dataStream.receiveData(source, length); + if (inFinished) { + dataStream.receiveFin(); + } + } + + @Override public void headers(boolean outFinished, boolean inFinished, int streamId, + int associatedStreamId, List

headerBlock, HeadersMode headersMode) { + if (pushedStream(streamId)) { + pushHeadersLater(streamId, headerBlock, inFinished); + return; + } + SpdyStream stream; + synchronized (SpdyConnection.this) { + // 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, headerBlock); + lastGoodStreamId = streamId; + streams.put(streamId, newStream); + executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) { + @Override public void execute() { + try { + handler.receive(newStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + return; + } + } + + // 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; + } + + // Update an existing stream. + stream.receiveHeaders(headerBlock, headersMode); + if (inFinished) stream.receiveFin(); + } + + @Override public void rstStream(int streamId, ErrorCode errorCode) { + if (pushedStream(streamId)) { + pushResetLater(streamId, errorCode); + return; + } + SpdyStream rstStream = removeStream(streamId); + if (rstStream != null) { + rstStream.receiveRstStream(errorCode); + } + } + + @Override public void settings(boolean clearPrevious, Settings newSettings) { + long delta = 0; + SpdyStream[] streamsToNotify = null; + synchronized (SpdyConnection.this) { + int priorWriteWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); + if (clearPrevious) peerSettings.clear(); + peerSettings.merge(newSettings); + if (getProtocol() == Protocol.HTTP_2) { + ackSettingsLater(); + } + int peerInitialWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); + if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) { + delta = peerInitialWindowSize - priorWriteWindowSize; + if (!receivedInitialPeerSettings) { + addBytesToWriteWindow(delta); + receivedInitialPeerSettings = true; + } + if (!streams.isEmpty()) { + streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]); + } + } + } + if (streamsToNotify != null && delta != 0) { + for (SpdyStream stream : streams.values()) { + synchronized (stream) { + stream.addBytesToWriteWindow(delta); + } + } + } + } + + private void ackSettingsLater() { + executor.submit(new NamedRunnable("OkHttp %s ACK Settings", hostName) { + @Override public void execute() { + try { + frameWriter.ackSettings(); + } catch (IOException ignored) { + } + } + }); + } + + @Override public void ackSettings() { + // TODO: If we don't get this callback after sending settings to the peer, SETTINGS_TIMEOUT. + } + + @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 lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { + if (debugData.size() > 0) { // TODO: log the debugData + } + synchronized (SpdyConnection.this) { + shutdown = true; + + // Fail all streams created after the last good stream ID. + for (Iterator> i = streams.entrySet().iterator(); + i.hasNext(); ) { + Map.Entry entry = i.next(); + int streamId = entry.getKey(); + if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) { + entry.getValue().receiveRstStream(ErrorCode.REFUSED_STREAM); + i.remove(); + } + } + } + } + + @Override public void windowUpdate(int streamId, long windowSizeIncrement) { + if (streamId == 0) { + synchronized (SpdyConnection.this) { + bytesLeftInWriteWindow += windowSizeIncrement; + SpdyConnection.this.notifyAll(); + } + } else { + SpdyStream stream = getStream(streamId); + if (stream != null) { + synchronized (stream) { + stream.addBytesToWriteWindow(windowSizeIncrement); + } + } + } + } + + @Override public void priority(int streamId, int streamDependency, int weight, + boolean exclusive) { + // TODO: honor priority. + } + + @Override + public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) { + pushRequestLater(promisedStreamId, requestHeaders); + } + + @Override public void alternateService(int streamId, String origin, ByteString protocol, + String host, int port, long maxAge) { + // TODO: register alternate service. + } + } + + /** Even, positive numbered streams are pushed streams in HTTP/2. */ + private boolean pushedStream(int streamId) { + return protocol == Protocol.HTTP_2 && streamId != 0 && (streamId & 1) == 0; + } + + // Guarded by this. + private final Set currentPushRequests = new LinkedHashSet(); + + private void pushRequestLater(final int streamId, final List
requestHeaders) { + synchronized (this) { + if (currentPushRequests.contains(streamId)) { + writeSynResetLater(streamId, ErrorCode.PROTOCOL_ERROR); + return; + } + currentPushRequests.add(streamId); + } + pushExecutor.submit(new NamedRunnable("OkHttp %s Push Request[%s]", hostName, streamId) { + @Override public void execute() { + boolean cancel = pushObserver.onRequest(streamId, requestHeaders); + try { + if (cancel) { + frameWriter.rstStream(streamId, ErrorCode.CANCEL); + synchronized (SpdyConnection.this) { + currentPushRequests.remove(streamId); + } + } + } catch (IOException ignored) { + } + } + }); + } + + private void pushHeadersLater(final int streamId, final List
requestHeaders, + final boolean inFinished) { + pushExecutor.submit(new NamedRunnable("OkHttp %s Push Headers[%s]", hostName, streamId) { + @Override public void execute() { + boolean cancel = pushObserver.onHeaders(streamId, requestHeaders, inFinished); + try { + if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL); + if (cancel || inFinished) { + synchronized (SpdyConnection.this) { + currentPushRequests.remove(streamId); + } + } + } catch (IOException ignored) { + } + } + }); + } + + /** + * Eagerly reads {@code byteCount} bytes from the source before launching a background task to + * process the data. This avoids corrupting the stream. + */ + private void pushDataLater(final int streamId, final BufferedSource source, final int byteCount, + final boolean inFinished) throws IOException { + final Buffer buffer = new Buffer(); + source.require(byteCount); // Eagerly read the frame before firing client thread. + source.read(buffer, byteCount); + if (buffer.size() != byteCount) throw new IOException(buffer.size() + " != " + byteCount); + pushExecutor.submit(new NamedRunnable("OkHttp %s Push Data[%s]", hostName, streamId) { + @Override public void execute() { + try { + boolean cancel = pushObserver.onData(streamId, buffer, byteCount, inFinished); + if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL); + if (cancel || inFinished) { + synchronized (SpdyConnection.this) { + currentPushRequests.remove(streamId); + } + } + } catch (IOException ignored) { + } + } + }); + } + + private void pushResetLater(final int streamId, final ErrorCode errorCode) { + pushExecutor.submit(new NamedRunnable("OkHttp %s Push Reset[%s]", hostName, streamId) { + @Override public void execute() { + pushObserver.onReset(streamId, errorCode); + synchronized (SpdyConnection.this) { + currentPushRequests.remove(streamId); + } + } + }); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyStream.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyStream.java new file mode 100644 index 000000000..db1a487f6 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyStream.java @@ -0,0 +1,577 @@ +/* + * 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.http.spdy.okio.AsyncTimeout; +import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; +import com.koushikdutta.async.http.spdy.okio.Sink; +import com.koushikdutta.async.http.spdy.okio.Source; +import com.koushikdutta.async.http.spdy.okio.Timeout; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.ArrayList; +import java.util.List; + +import static com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE; + +/** A logical bidirectional stream. */ +public final class SpdyStream { + // Internal state is guarded by this. No long-running or potentially + // blocking operations are performed while the lock is held. + + /** + * The total number of bytes consumed by the application (with {@link + * SpdyDataSource#read}), but not yet acknowledged by sending a {@code + * WINDOW_UPDATE} frame on this stream. + */ + // Visible for testing + long unacknowledgedBytesRead = 0; + + /** + * Count of bytes that can be written on the stream before receiving a + * window update. Even if this is positive, writes will block until there + * available bytes in {@code connection.bytesLeftInWriteWindow}. + */ + // guarded by this + long bytesLeftInWriteWindow; + + private final int id; + private final SpdyConnection connection; + private long readTimeoutMillis = 0; + + /** Headers sent by the stream initiator. Immutable and non null. */ + private final List
requestHeaders; + + /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */ + private List
responseHeaders; + + private final SpdyDataSource source; + final SpdyDataSink sink; + private final SpdyTimeout readTimeout = new SpdyTimeout(); + private final SpdyTimeout writeTimeout = new SpdyTimeout(); + + /** + * The reason why this stream was abnormally closed. If there are multiple + * reasons to abnormally close this stream (such as both peers closing it + * near-simultaneously) then this is the first reason known to this peer. + */ + private ErrorCode errorCode = null; + + SpdyStream(int id, SpdyConnection connection, boolean outFinished, boolean inFinished, + List
requestHeaders) { + if (connection == null) throw new NullPointerException("connection == null"); + if (requestHeaders == null) throw new NullPointerException("requestHeaders == null"); + this.id = id; + this.connection = connection; + this.bytesLeftInWriteWindow = + connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); + this.source = new SpdyDataSource( + connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE)); + this.sink = new SpdyDataSink(); + this.source.finished = inFinished; + this.sink.finished = outFinished; + this.requestHeaders = requestHeaders; + } + + public int getId() { + return id; + } + + /** + * Returns true if this stream is open. A stream is open until either: + *
    + *
  • A {@code SYN_RESET} frame abnormally terminates the stream. + *
  • Both input and output streams have transmitted all data and + * headers. + *
+ * Note that the input stream may continue to yield data even after a stream + * reports itself as not open. This is because input data is buffered. + */ + public synchronized boolean isOpen() { + if (errorCode != null) { + return false; + } + if ((source.finished || source.closed) + && (sink.finished || sink.closed) + && responseHeaders != null) { + return false; + } + return true; + } + + /** Returns true if this stream was created by this peer. */ + public boolean isLocallyInitiated() { + boolean streamIsClient = ((id & 1) == 1); + return connection.client == streamIsClient; + } + + public SpdyConnection getConnection() { + return connection; + } + + public List
getRequestHeaders() { + return requestHeaders; + } + + /** + * Returns the stream's response headers, blocking if necessary if they + * have not been received yet. + */ + public synchronized List
getResponseHeaders() throws IOException { + readTimeout.enter(); + try { + while (responseHeaders == null && errorCode == null) { + waitForIo(); + } + } finally { + readTimeout.exitAndThrowIfTimedOut(); + } + if (responseHeaders != null) return responseHeaders; + throw new IOException("stream was reset: " + errorCode); + } + + /** + * Returns the reason why this stream was closed, or null if it closed + * normally or has not yet been closed. + */ + public synchronized ErrorCode getErrorCode() { + return errorCode; + } + + /** + * Sends a reply to an incoming stream. + * + * @param out true to create an output stream that we can use to send data + * to the remote peer. Corresponds to {@code FLAG_FIN}. + */ + public void reply(List
responseHeaders, boolean out) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean outFinished = false; + synchronized (this) { + if (responseHeaders == null) { + throw new NullPointerException("responseHeaders == null"); + } + if (this.responseHeaders != null) { + throw new IllegalStateException("reply already sent"); + } + this.responseHeaders = responseHeaders; + if (!out) { + this.sink.finished = true; + outFinished = true; + } + } + connection.writeSynReply(id, outFinished, responseHeaders); + + if (outFinished) { + connection.flush(); + } + } + + public Timeout readTimeout() { + return readTimeout; + } + + public Timeout writeTimeout() { + return writeTimeout; + } + + /** Returns a source that reads data from the peer. */ + public Source getSource() { + return source; + } + + /** + * Returns a sink that can be used to write data to the peer. + * + * @throws IllegalStateException if this stream was initiated by the peer + * and a {@link #reply} has not yet been sent. + */ + public Sink getSink() { + synchronized (this) { + if (responseHeaders == null && !isLocallyInitiated()) { + throw new IllegalStateException("reply before requesting the sink"); + } + } + return sink; + } + + /** + * Abnormally terminate this stream. This blocks until the {@code RST_STREAM} + * frame has been transmitted. + */ + public void close(ErrorCode rstStatusCode) throws IOException { + if (!closeInternal(rstStatusCode)) { + return; // Already closed. + } + connection.writeSynReset(id, rstStatusCode); + } + + /** + * Abnormally terminate this stream. This enqueues a {@code RST_STREAM} + * frame and returns immediately. + */ + public void closeLater(ErrorCode errorCode) { + if (!closeInternal(errorCode)) { + return; // Already closed. + } + connection.writeSynResetLater(id, errorCode); + } + + /** Returns true if this stream was closed. */ + private boolean closeInternal(ErrorCode errorCode) { + assert (!Thread.holdsLock(this)); + synchronized (this) { + if (this.errorCode != null) { + return false; + } + if (source.finished && sink.finished) { + return false; + } + this.errorCode = errorCode; + notifyAll(); + } + connection.removeStream(id); + return true; + } + + void receiveHeaders(List
headers, HeadersMode headersMode) { + assert (!Thread.holdsLock(SpdyStream.this)); + ErrorCode errorCode = null; + boolean open = true; + synchronized (this) { + if (responseHeaders == null) { + if (headersMode.failIfHeadersAbsent()) { + errorCode = ErrorCode.PROTOCOL_ERROR; + } else { + responseHeaders = headers; + open = isOpen(); + notifyAll(); + } + } else { + if (headersMode.failIfHeadersPresent()) { + errorCode = ErrorCode.STREAM_IN_USE; + } else { + List
newHeaders = new ArrayList
(); + newHeaders.addAll(responseHeaders); + newHeaders.addAll(headers); + this.responseHeaders = newHeaders; + } + } + } + if (errorCode != null) { + closeLater(errorCode); + } else if (!open) { + connection.removeStream(id); + } + } + + void receiveData(BufferedSource in, int length) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + this.source.receive(in, length); + } + + void receiveFin() { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean open; + synchronized (this) { + this.source.finished = true; + open = isOpen(); + notifyAll(); + } + if (!open) { + connection.removeStream(id); + } + } + + synchronized void receiveRstStream(ErrorCode errorCode) { + if (this.errorCode == null) { + this.errorCode = errorCode; + notifyAll(); + } + } + + /** + * A source that reads the incoming data frames of a stream. Although this + * class uses synchronization to safely receive incoming data frames, it is + * not intended for use by multiple readers. + */ + private final class SpdyDataSource implements Source { + /** Buffer to receive data from the network into. Only accessed by the reader thread. */ + private final Buffer receiveBuffer = new Buffer(); + + /** Buffer with readable data. Guarded by SpdyStream.this. */ + private final Buffer readBuffer = new Buffer(); + + /** Maximum number of bytes to buffer before reporting a flow control error. */ + private final long maxByteCount; + + /** True if the caller has closed this stream. */ + private boolean closed; + + /** + * True if either side has cleanly shut down this stream. We will + * receive no more bytes beyond those already in the buffer. + */ + private boolean finished; + + private SpdyDataSource(long maxByteCount) { + this.maxByteCount = maxByteCount; + } + + @Override public long read(Buffer sink, long byteCount) + throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + + long read; + synchronized (SpdyStream.this) { + waitUntilReadable(); + checkNotClosed(); + if (readBuffer.size() == 0) return -1; // This source is exhausted. + + // Move bytes from the read buffer into the caller's buffer. + read = readBuffer.read(sink, Math.min(byteCount, readBuffer.size())); + + // Flow control: notify the peer that we're ready for more data! + unacknowledgedBytesRead += read; + if (unacknowledgedBytesRead + >= connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) { + connection.writeWindowUpdateLater(id, unacknowledgedBytesRead); + unacknowledgedBytesRead = 0; + } + } + + // Update connection.unacknowledgedBytesRead outside the stream lock. + synchronized (connection) { // Multiple application threads may hit this section. + connection.unacknowledgedBytesRead += read; + if (connection.unacknowledgedBytesRead + >= connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) { + connection.writeWindowUpdateLater(0, connection.unacknowledgedBytesRead); + connection.unacknowledgedBytesRead = 0; + } + } + + return read; + } + + /** Returns once the source is either readable or finished. */ + private void waitUntilReadable() throws IOException { + readTimeout.enter(); + try { + while (readBuffer.size() == 0 && !finished && !closed && errorCode == null) { + waitForIo(); + } + } finally { + readTimeout.exitAndThrowIfTimedOut(); + } + } + + void receive(BufferedSource in, long byteCount) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + + while (byteCount > 0) { + boolean finished; + boolean flowControlError; + synchronized (SpdyStream.this) { + finished = this.finished; + flowControlError = byteCount + readBuffer.size() > maxByteCount; + } + + // If the peer sends more data than we can handle, discard it and close the connection. + if (flowControlError) { + in.skip(byteCount); + closeLater(ErrorCode.FLOW_CONTROL_ERROR); + return; + } + + // Discard data received after the stream is finished. It's probably a benign race. + if (finished) { + in.skip(byteCount); + return; + } + + // Fill the receive buffer without holding any locks. + long read = in.read(receiveBuffer, byteCount); + if (read == -1) throw new EOFException(); + byteCount -= read; + + // Move the received data to the read buffer to the reader can read it. + synchronized (SpdyStream.this) { + boolean wasEmpty = readBuffer.size() == 0; + readBuffer.writeAll(receiveBuffer); + if (wasEmpty) { + SpdyStream.this.notifyAll(); + } + } + } + } + + @Override public Timeout timeout() { + return readTimeout; + } + + @Override public void close() throws IOException { + synchronized (SpdyStream.this) { + closed = true; + readBuffer.clear(); + SpdyStream.this.notifyAll(); + } + cancelStreamIfNecessary(); + } + + private void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("stream closed"); + } + if (errorCode != null) { + throw new IOException("stream was reset: " + errorCode); + } + } + } + + private void cancelStreamIfNecessary() throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean open; + boolean cancel; + synchronized (this) { + cancel = !source.finished && source.closed && (sink.finished || sink.closed); + open = isOpen(); + } + if (cancel) { + // RST this stream to prevent additional data from being sent. This + // is safe because the input stream is closed (we won't use any + // further bytes) and the output stream is either finished or closed + // (so RSTing both streams doesn't cause harm). + SpdyStream.this.close(ErrorCode.CANCEL); + } else if (!open) { + connection.removeStream(id); + } + } + + /** + * A sink that writes outgoing data frames of a stream. This class is not + * thread safe. + */ + final class SpdyDataSink implements Sink { + private boolean closed; + + /** + * True if either side has cleanly shut down this stream. We shall send + * no more bytes. + */ + private boolean finished; + + @Override public void write(Buffer source, long byteCount) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + while (byteCount > 0) { + long toWrite; + synchronized (SpdyStream.this) { + writeTimeout.enter(); + try { + while (bytesLeftInWriteWindow <= 0 && !finished && !closed && errorCode == null) { + waitForIo(); // Wait until we receive a WINDOW_UPDATE. + } + } finally { + writeTimeout.exitAndThrowIfTimedOut(); + } + + checkOutNotClosed(); // Kick out if the stream was reset or closed while waiting. + toWrite = Math.min(bytesLeftInWriteWindow, byteCount); + bytesLeftInWriteWindow -= toWrite; + } + + byteCount -= toWrite; + connection.writeData(id, false, source, toWrite); + } + } + + @Override public void flush() throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + synchronized (SpdyStream.this) { + checkOutNotClosed(); + } + connection.flush(); + } + + @Override public Timeout timeout() { + return writeTimeout; + } + + @Override public void close() throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + synchronized (SpdyStream.this) { + if (closed) return; + } + if (!sink.finished) { + connection.writeData(id, true, null, 0); + } + synchronized (SpdyStream.this) { + closed = true; + } + connection.flush(); + cancelStreamIfNecessary(); + } + } + + /** + * {@code delta} will be negative if a settings frame initial window is + * smaller than the last. + */ + void addBytesToWriteWindow(long delta) { + bytesLeftInWriteWindow += delta; + if (delta > 0) SpdyStream.this.notifyAll(); + } + + private void checkOutNotClosed() throws IOException { + if (sink.closed) { + throw new IOException("stream closed"); + } else if (sink.finished) { + throw new IOException("stream finished"); + } else if (errorCode != null) { + throw new IOException("stream was reset: " + errorCode); + } + } + + /** + * Like {@link #wait}, but throws an {@code InterruptedIOException} when + * interrupted instead of the more awkward {@link InterruptedException}. + */ + private void waitForIo() throws InterruptedIOException { + try { + wait(); + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + } + + /** + * The Okio timeout watchdog will call {@link #timedOut} if the timeout is + * reached. In that case we close the stream (asynchronously) which will + * notify the waiting thread. + */ + class SpdyTimeout extends AsyncTimeout { + @Override protected void timedOut() { + closeLater(ErrorCode.CANCEL); + } + + public void exitAndThrowIfTimedOut() throws InterruptedIOException { + if (exit()) throw new InterruptedIOException("timeout"); + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java new file mode 100644 index 000000000..56994d177 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; + + +import com.koushikdutta.async.http.Protocol; +import com.koushikdutta.async.http.spdy.okio.BufferedSink; +import com.koushikdutta.async.http.spdy.okio.BufferedSource; + +/** A version and dialect of the framed socket protocol. */ +public interface Variant { + + /** The protocol as selected using NPN or ALPN. */ + Protocol getProtocol(); + + /** + * @param client true if this is the HTTP client's reader, reading frames from a server. + */ + FrameReader newReader(BufferedSource source, boolean client); + + /** + * @param client true if this is the HTTP client's writer, writing frames to a server. + */ + FrameWriter newWriter(BufferedSink sink, boolean client); + + int maxFrameSize(); +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/AsyncTimeout.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/AsyncTimeout.java new file mode 100644 index 000000000..b0b46ff59 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/AsyncTimeout.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.IOException; +import java.io.InterruptedIOException; + +/** + * This timeout uses a background thread to take action exactly when the timeout + * occurs. Use this to implement timeouts where they aren't supported natively, + * such as to sockets that are blocked on writing. + * + *

Subclasses should override {@link #timedOut} to take action when a timeout + * occurs. This method will be invoked by the shared watchdog thread so it + * should not do any long-running operations. Otherwise we risk starving other + * timeouts from being triggered. + * + *

Use {@link #sink} and {@link #source} to apply this timeout to a stream. + * The returned value will apply the timeout to each operation on the wrapped + * stream. + * + *

Callers should call {@link #enter} before doing work that is subject to + * timeouts, and {@link #exit} afterwards. The return value of {@link #exit} + * indicates whether a timeout was triggered. Note that the call to {@link + * #timedOut} is asynchronous, and may be called after {@link #exit}. + */ +public class AsyncTimeout extends Timeout { + /** + * The watchdog thread processes a linked list of pending timeouts, sorted in + * the order to be triggered. This class synchronizes on AsyncTimeout.class. + * This lock guards the queue. + * + *

Head's 'next' points to the first element of the linked list. The first + * element is the next node to time out, or null if the queue is empty. The + * head is null until the watchdog thread is started. + */ + private static AsyncTimeout head; + + /** True if this node is currently in the queue. */ + private boolean inQueue; + + /** The next node in the linked list. */ + private AsyncTimeout next; + + /** If scheduled, this is the time that the watchdog should time this out. */ + private long timeoutAt; + + public final void enter() { + if (inQueue) throw new IllegalStateException("Unbalanced enter/exit"); + long timeoutNanos = timeoutNanos(); + boolean hasDeadline = hasDeadline(); + if (timeoutNanos == 0 && !hasDeadline) { + return; // No timeout and no deadline? Don't bother with the queue. + } + inQueue = true; + scheduleTimeout(this, timeoutNanos, hasDeadline); + } + + private static synchronized void scheduleTimeout( + AsyncTimeout node, long timeoutNanos, boolean hasDeadline) { + // Start the watchdog thread and create the head node when the first timeout is scheduled. + if (head == null) { + head = new AsyncTimeout(); + new Watchdog().start(); + } + + long now = System.nanoTime(); + if (timeoutNanos != 0 && hasDeadline) { + // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap around, + // Math.min() is undefined for absolute values, but meaningful for relative ones. + node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now); + } else if (timeoutNanos != 0) { + node.timeoutAt = now + timeoutNanos; + } else if (hasDeadline) { + node.timeoutAt = node.deadlineNanoTime(); + } else { + throw new AssertionError(); + } + + // Insert the node in sorted order. + long remainingNanos = node.remainingNanos(now); + for (AsyncTimeout prev = head; true; prev = prev.next) { + if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) { + node.next = prev.next; + prev.next = node; + if (prev == head) { + AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front. + } + break; + } + } + } + + /** Returns true if the timeout occurred. */ + public final boolean exit() { + if (!inQueue) return false; + inQueue = false; + return cancelScheduledTimeout(this); + } + + /** Returns true if the timeout occurred. */ + private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) { + // Remove the node from the linked list. + for (AsyncTimeout prev = head; prev != null; prev = prev.next) { + if (prev.next == node) { + prev.next = node.next; + node.next = null; + return false; + } + } + + // The node wasn't found in the linked list: it must have timed out! + return true; + } + + /** + * Returns the amount of time left until the time out. This will be negative + * if the timeout has elapsed and the timeout should occur immediately. + */ + private long remainingNanos(long now) { + return timeoutAt - now; + } + + /** + * Invoked by the watchdog thread when the time between calls to {@link + * #enter()} and {@link #exit()} has exceeded the timeout. + */ + protected void timedOut() { + } + + /** + * Returns a new sink that delegates to {@code sink}, using this to implement + * timeouts. This works best if {@link #timedOut} is overridden to interrupt + * {@code sink}'s current operation. + */ + public final Sink sink(final Sink sink) { + return new Sink() { + @Override public void write(Buffer source, long byteCount) throws IOException { + boolean throwOnTimeout = false; + enter(); + try { + sink.write(source, byteCount); + throwOnTimeout = true; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public void flush() throws IOException { + boolean throwOnTimeout = false; + enter(); + try { + sink.flush(); + throwOnTimeout = true; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public void close() throws IOException { + boolean throwOnTimeout = false; + enter(); + try { + sink.close(); + throwOnTimeout = true; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public Timeout timeout() { + return AsyncTimeout.this; + } + + @Override public String toString() { + return "AsyncTimeout.sink(" + sink + ")"; + } + }; + } + + /** + * Returns a new source that delegates to {@code source}, using this to + * implement timeouts. This works best if {@link #timedOut} is overridden to + * interrupt {@code sink}'s current operation. + */ + public final Source source(final Source source) { + return new Source() { + @Override public long read(Buffer sink, long byteCount) throws IOException { + boolean throwOnTimeout = false; + enter(); + try { + long result = source.read(sink, byteCount); + throwOnTimeout = true; + return result; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public void close() throws IOException { + boolean throwOnTimeout = false; + try { + source.close(); + throwOnTimeout = true; + } catch (IOException e) { + throw exit(e); + } finally { + exit(throwOnTimeout); + } + } + + @Override public Timeout timeout() { + return AsyncTimeout.this; + } + + @Override public String toString() { + return "AsyncTimeout.source(" + source + ")"; + } + }; + } + + /** + * Throws an InterruptedIOException if {@code throwOnTimeout} is true and a + * timeout occurred. + */ + final void exit(boolean throwOnTimeout) throws IOException { + boolean timedOut = exit(); + if (timedOut && throwOnTimeout) throw new InterruptedIOException("timeout"); + } + + /** + * Returns either {@code cause} or an InterruptedIOException that's caused by + * {@code cause} if a timeout occurred. + */ + final IOException exit(IOException cause) throws IOException { + if (!exit()) return cause; + InterruptedIOException e = new InterruptedIOException("timeout"); + e.initCause(cause); + return e; + } + + private static final class Watchdog extends Thread { + public Watchdog() { + super("Okio Watchdog"); + setDaemon(true); + } + + public void run() { + while (true) { + try { + AsyncTimeout timedOut = awaitTimeout(); + + // Didn't find a node to interrupt. Try again. + if (timedOut == null) continue; + + // Close the timed out node. + timedOut.timedOut(); + } catch (InterruptedException ignored) { + } + } + } + } + + /** + * Removes and returns the node at the head of the list, waiting for it to + * time out if necessary. Returns null if the situation changes while waiting: + * either a newer node is inserted at the head, or the node being waited on + * has been removed. + */ + private static synchronized AsyncTimeout awaitTimeout() throws InterruptedException { + // Get the next eligible node. + AsyncTimeout node = head.next; + + // The queue is empty. Wait for something to be enqueued. + if (node == null) { + AsyncTimeout.class.wait(); + return null; + } + + long waitNanos = node.remainingNanos(System.nanoTime()); + + // The head of the queue hasn't timed out yet. Await that. + if (waitNanos > 0) { + // Waiting is made complicated by the fact that we work in nanoseconds, + // but the API wants (millis, nanos) in two arguments. + long waitMillis = waitNanos / 1000000L; + waitNanos -= (waitMillis * 1000000L); + AsyncTimeout.class.wait(waitMillis, (int) waitNanos); + return null; + } + + // The head of the queue has timed out. Remove it. + head.next = node.next; + node.next = null; + return node; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Base64.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Base64.java new file mode 100644 index 000000000..c0a6571f2 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Base64.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author Alexander Y. Kleymenov + */ +package com.koushikdutta.async.http.spdy.okio; + +import java.io.UnsupportedEncodingException; + +final class Base64 { + private Base64() { + } + + public static byte[] decode(String in) { + // Ignore trailing '=' padding and whitespace from the input. + int limit = in.length(); + for (; limit > 0; limit--) { + char c = in.charAt(limit - 1); + if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') { + break; + } + } + + // If the input includes whitespace, this output array will be longer than necessary. + byte[] out = new byte[(int) (limit * 6L / 8L)]; + int outCount = 0; + int inCount = 0; + + int word = 0; + for (int pos = 0; pos < limit; pos++) { + char c = in.charAt(pos); + + int bits; + if (c >= 'A' && c <= 'Z') { + // char ASCII value + // A 65 0 + // Z 90 25 (ASCII - 65) + bits = c - 65; + } else if (c >= 'a' && c <= 'z') { + // char ASCII value + // a 97 26 + // z 122 51 (ASCII - 71) + bits = c - 71; + } else if (c >= '0' && c <= '9') { + // char ASCII value + // 0 48 52 + // 9 57 61 (ASCII + 4) + bits = c + 4; + } else if (c == '+') { + bits = 62; + } else if (c == '/') { + bits = 63; + } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') { + continue; + } else { + return null; + } + + // Append this char's 6 bits to the word. + word = (word << 6) | (byte) bits; + + // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes. + inCount++; + if (inCount % 4 == 0) { + out[outCount++] = (byte) (word >> 16); + out[outCount++] = (byte) (word >> 8); + out[outCount++] = (byte) word; + } + } + + int lastWordChars = inCount % 4; + if (lastWordChars == 1) { + // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail. + return null; + } else if (lastWordChars == 2) { + // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits. + word = word << 12; + out[outCount++] = (byte) (word >> 16); + } else if (lastWordChars == 3) { + // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits. + word = word << 6; + out[outCount++] = (byte) (word >> 16); + out[outCount++] = (byte) (word >> 8); + } + + // If we sized our out array perfectly, we're done. + if (outCount == out.length) return out; + + // Copy the decoded bytes to a new, right-sized array. + byte[] prefix = new byte[outCount]; + System.arraycopy(out, 0, prefix, 0, outCount); + return prefix; + } + + private static final byte[] MAP = new byte[] { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', + '5', '6', '7', '8', '9', '+', '/' + }; + + public static String encode(byte[] in) { + int length = (in.length + 2) * 4 / 3; + byte[] out = new byte[length]; + int index = 0, end = in.length - in.length % 3; + for (int i = 0; i < end; i += 3) { + out[index++] = MAP[(in[i] & 0xff) >> 2]; + out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; + out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; + out[index++] = MAP[(in[i + 2] & 0x3f)]; + } + switch (in.length % 3) { + case 1: + out[index++] = MAP[(in[end] & 0xff) >> 2]; + out[index++] = MAP[(in[end] & 0x03) << 4]; + out[index++] = '='; + out[index++] = '='; + break; + case 2: + out[index++] = MAP[(in[end] & 0xff) >> 2]; + out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; + out[index++] = MAP[((in[end + 1] & 0x0f) << 2)]; + out[index++] = '='; + break; + } + try { + return new String(out, 0, index, "US-ASCII"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java new file mode 100644 index 000000000..4ac22b15e --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java @@ -0,0 +1,911 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +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 static com.koushikdutta.async.http.spdy.okhttp.internal.Util.checkOffsetAndCount; +import static com.koushikdutta.async.http.spdy.okio.Util.reverseBytesLong; + +/** + * A collection of bytes in memory. + * + *

Moving data from one buffer to another is fast. Instead + * of copying bytes from one place in memory to another, this class just changes + * ownership of the underlying byte arrays. + * + *

This buffer grows with your data. Just like ArrayList, + * each buffer starts small. It consumes only the memory it needs to. + * + *

This buffer pools its byte arrays. When you allocate a + * byte array in Java, the runtime must zero-fill the requested array before + * returning it to you. Even if you're going to write over that space anyway. + * This class avoids zero-fill and GC churn by pooling byte arrays. + */ +public final class Buffer implements BufferedSource, BufferedSink, Cloneable { + Segment head; + long size; + + public Buffer() { + } + + /** Returns the number of bytes currently in this buffer. */ + public long size() { + return size; + } + + @Override public Buffer buffer() { + return this; + } + + @Override public OutputStream outputStream() { + return new OutputStream() { + @Override public void write(int b) { + writeByte((byte) b); + } + + @Override public void write(byte[] data, int offset, int byteCount) { + Buffer.this.write(data, offset, byteCount); + } + + @Override public void flush() { + } + + @Override public void close() { + } + + @Override public String toString() { + return this + ".outputStream()"; + } + }; + } + + @Override public Buffer emitCompleteSegments() { + return this; // Nowhere to emit to! + } + + @Override public boolean exhausted() { + return size == 0; + } + + @Override public void require(long byteCount) throws EOFException { + if (this.size < byteCount) throw new EOFException(); + } + + @Override public InputStream inputStream() { + return new InputStream() { + @Override public int read() { + if (size > 0) return readByte() & 0xff; + return -1; + } + + @Override public int read(byte[] sink, int offset, int byteCount) { + return Buffer.this.read(sink, offset, byteCount); + } + + @Override public int available() { + return (int) Math.min(size, Integer.MAX_VALUE); + } + + @Override public void close() { + } + + @Override public String toString() { + return Buffer.this + ".inputStream()"; + } + }; + } + + /** Copy the contents of this to {@code out}. */ + public Buffer copyTo(OutputStream out) throws IOException { + return copyTo(out, 0, size); + } + + /** + * Copy {@code byteCount} bytes from this, starting at {@code offset}, to + * {@code out}. + */ + public Buffer copyTo(OutputStream out, long offset, long byteCount) throws IOException { + if (out == null) throw new IllegalArgumentException("out == null"); + checkOffsetAndCount(size, offset, byteCount); + if (byteCount == 0) return this; + + // Skip segments that we aren't copying from. + Segment s = head; + for (; offset >= (s.limit - s.pos); s = s.next) { + offset -= (s.limit - s.pos); + } + + // Copy from one segment at a time. + for (; byteCount > 0; s = s.next) { + int pos = (int) (s.pos + offset); + int toWrite = (int) Math.min(s.limit - pos, byteCount); + out.write(s.data, pos, toWrite); + byteCount -= toWrite; + offset = 0; + } + + return this; + } + + /** Write the contents of this to {@code out}. */ + public Buffer writeTo(OutputStream out) throws IOException { + return writeTo(out, size); + } + + /** Write {@code byteCount} bytes from this to {@code out}. */ + public Buffer writeTo(OutputStream out, long byteCount) throws IOException { + if (out == null) throw new IllegalArgumentException("out == null"); + checkOffsetAndCount(size, 0, byteCount); + + Segment s = head; + while (byteCount > 0) { + int toCopy = (int) Math.min(byteCount, s.limit - s.pos); + out.write(s.data, s.pos, toCopy); + + s.pos += toCopy; + size -= toCopy; + byteCount -= toCopy; + + if (s.pos == s.limit) { + Segment toRecycle = s; + head = s = toRecycle.pop(); + SegmentPool.getInstance().recycle(toRecycle); + } + } + + return this; + } + + /** Read and exhaust bytes from {@code in} to this. */ + public Buffer readFrom(InputStream in) throws IOException { + readFrom(in, Long.MAX_VALUE, true); + return this; + } + + /** Read {@code byteCount} bytes from {@code in} to this. */ + public Buffer readFrom(InputStream in, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + readFrom(in, byteCount, false); + return this; + } + + private void readFrom(InputStream in, long byteCount, boolean forever) throws IOException { + if (in == null) throw new IllegalArgumentException("in == null"); + while (byteCount > 0 || forever) { + Segment tail = writableSegment(1); + int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit); + int bytesRead = in.read(tail.data, tail.limit, maxToCopy); + if (bytesRead == -1) { + if (forever) return; + throw new EOFException(); + } + tail.limit += bytesRead; + size += bytesRead; + byteCount -= bytesRead; + } + } + + /** + * Returns the number of bytes in segments that are not writable. This is the + * number of bytes that can be flushed immediately to an underlying sink + * without harming throughput. + */ + public long completeSegmentByteCount() { + long result = size; + if (result == 0) return 0; + + // Omit the tail if it's still writable. + Segment tail = head.prev; + if (tail.limit < Segment.SIZE) { + result -= tail.limit - tail.pos; + } + + return result; + } + + @Override public byte readByte() { + if (size == 0) throw new IllegalStateException("size == 0"); + + Segment segment = head; + int pos = segment.pos; + int limit = segment.limit; + + byte[] data = segment.data; + byte b = data[pos++]; + size -= 1; + + if (pos == limit) { + head = segment.pop(); + SegmentPool.getInstance().recycle(segment); + } else { + segment.pos = pos; + } + + return b; + } + + /** Returns the byte at {@code pos}. */ + public byte getByte(long pos) { + checkOffsetAndCount(size, pos, 1); + for (Segment s = head; true; s = s.next) { + int segmentByteCount = s.limit - s.pos; + if (pos < segmentByteCount) return s.data[s.pos + (int) pos]; + pos -= segmentByteCount; + } + } + + @Override public short readShort() { + if (size < 2) throw new IllegalStateException("size < 2: " + size); + + Segment segment = head; + int pos = segment.pos; + int limit = segment.limit; + + // If the short is split across multiple segments, delegate to readByte(). + if (limit - pos < 2) { + int s = (readByte() & 0xff) << 8 + | (readByte() & 0xff); + return (short) s; + } + + byte[] data = segment.data; + int s = (data[pos++] & 0xff) << 8 + | (data[pos++] & 0xff); + size -= 2; + + if (pos == limit) { + head = segment.pop(); + SegmentPool.getInstance().recycle(segment); + } else { + segment.pos = pos; + } + + return (short) s; + } + + @Override public int readInt() { + if (size < 4) throw new IllegalStateException("size < 4: " + size); + + Segment segment = head; + int pos = segment.pos; + int limit = segment.limit; + + // If the int is split across multiple segments, delegate to readByte(). + if (limit - pos < 4) { + return (readByte() & 0xff) << 24 + | (readByte() & 0xff) << 16 + | (readByte() & 0xff) << 8 + | (readByte() & 0xff); + } + + byte[] data = segment.data; + int i = (data[pos++] & 0xff) << 24 + | (data[pos++] & 0xff) << 16 + | (data[pos++] & 0xff) << 8 + | (data[pos++] & 0xff); + size -= 4; + + if (pos == limit) { + head = segment.pop(); + SegmentPool.getInstance().recycle(segment); + } else { + segment.pos = pos; + } + + return i; + } + + @Override public long readLong() { + if (size < 8) throw new IllegalStateException("size < 8: " + size); + + Segment segment = head; + int pos = segment.pos; + int limit = segment.limit; + + // If the long is split across multiple segments, delegate to readInt(). + if (limit - pos < 8) { + return (readInt() & 0xffffffffL) << 32 + | (readInt() & 0xffffffffL); + } + + byte[] data = segment.data; + long v = (data[pos++] & 0xffL) << 56 + | (data[pos++] & 0xffL) << 48 + | (data[pos++] & 0xffL) << 40 + | (data[pos++] & 0xffL) << 32 + | (data[pos++] & 0xffL) << 24 + | (data[pos++] & 0xffL) << 16 + | (data[pos++] & 0xffL) << 8 + | (data[pos++] & 0xffL); + size -= 8; + + if (pos == limit) { + head = segment.pop(); + SegmentPool.getInstance().recycle(segment); + } else { + segment.pos = pos; + } + + return v; + } + + @Override public short readShortLe() { + return Util.reverseBytesShort(readShort()); + } + + @Override public int readIntLe() { + return Util.reverseBytesInt(readInt()); + } + + @Override public long readLongLe() { + return Util.reverseBytesLong(readLong()); + } + + @Override public ByteString readByteString() { + return new ByteString(readByteArray()); + } + + @Override public ByteString readByteString(long byteCount) throws EOFException { + return new ByteString(readByteArray(byteCount)); + } + + @Override public void readFully(Buffer sink, long byteCount) throws EOFException { + if (size < byteCount) { + sink.write(this, size); // Exhaust ourselves. + throw new EOFException(); + } + sink.write(this, byteCount); + } + + @Override public long readAll(Sink sink) throws IOException { + long byteCount = size; + if (byteCount > 0) { + sink.write(this, byteCount); + } + return byteCount; + } + + @Override public String readUtf8() { + try { + return readString(size, Util.UTF_8); + } catch (EOFException e) { + throw new AssertionError(e); + } + } + + @Override public String readUtf8(long byteCount) throws EOFException { + return readString(byteCount, Util.UTF_8); + } + + @Override public String readString(Charset charset) { + try { + return readString(size, charset); + } catch (EOFException e) { + throw new AssertionError(e); + } + } + + @Override public String readString(long byteCount, Charset charset) throws EOFException { + checkOffsetAndCount(size, 0, byteCount); + if (charset == null) throw new IllegalArgumentException("charset == null"); + if (byteCount > Integer.MAX_VALUE) { + throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount); + } + if (byteCount == 0) return ""; + + Segment head = this.head; + if (head.pos + byteCount > head.limit) { + // If the string spans multiple segments, delegate to readBytes(). + return new String(readByteArray(byteCount), charset); + } + + String result = new String(head.data, head.pos, (int) byteCount, charset); + head.pos += byteCount; + size -= byteCount; + + if (head.pos == head.limit) { + this.head = head.pop(); + SegmentPool.getInstance().recycle(head); + } + + return result; + } + + @Override public String readUtf8Line() throws EOFException { + long newline = indexOf((byte) '\n'); + + if (newline == -1) { + return size != 0 ? readUtf8(size) : null; + } + + return readUtf8Line(newline); + } + + @Override public String readUtf8LineStrict() throws EOFException { + long newline = indexOf((byte) '\n'); + if (newline == -1) throw new EOFException(); + return readUtf8Line(newline); + } + + String readUtf8Line(long newline) throws EOFException { + if (newline > 0 && getByte(newline - 1) == '\r') { + // Read everything until '\r\n', then skip the '\r\n'. + String result = readUtf8((newline - 1)); + skip(2); + return result; + + } else { + // Read everything until '\n', then skip the '\n'. + String result = readUtf8(newline); + skip(1); + return result; + } + } + + @Override public byte[] readByteArray() { + try { + return readByteArray(size); + } catch (EOFException e) { + throw new AssertionError(e); + } + } + + @Override public byte[] readByteArray(long byteCount) throws EOFException { + checkOffsetAndCount(this.size, 0, byteCount); + if (byteCount > Integer.MAX_VALUE) { + throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount); + } + + byte[] result = new byte[(int) byteCount]; + readFully(result); + return result; + } + + @Override public int read(byte[] sink) { + return read(sink, 0, sink.length); + } + + @Override public void readFully(byte[] sink) throws EOFException { + int offset = 0; + while (offset < sink.length) { + int read = read(sink, offset, sink.length - offset); + if (read == -1) throw new EOFException(); + offset += read; + } + } + + @Override public int read(byte[] sink, int offset, int byteCount) { + checkOffsetAndCount(sink.length, offset, byteCount); + + Segment s = this.head; + if (s == null) return -1; + int toCopy = Math.min(byteCount, s.limit - s.pos); + System.arraycopy(s.data, s.pos, sink, offset, toCopy); + + s.pos += toCopy; + this.size -= toCopy; + + if (s.pos == s.limit) { + this.head = s.pop(); + SegmentPool.getInstance().recycle(s); + } + + return toCopy; + } + + /** + * Discards all bytes in this buffer. Calling this method when you're done + * with a buffer will return its segments to the pool. + */ + public void clear() { + try { + skip(size); + } catch (EOFException e) { + throw new AssertionError(e); + } + } + + /** Discards {@code byteCount} bytes from the head of this buffer. */ + @Override public void skip(long byteCount) throws EOFException { + while (byteCount > 0) { + if (head == null) throw new EOFException(); + + int toSkip = (int) Math.min(byteCount, head.limit - head.pos); + size -= toSkip; + byteCount -= toSkip; + head.pos += toSkip; + + if (head.pos == head.limit) { + Segment toRecycle = head; + head = toRecycle.pop(); + SegmentPool.getInstance().recycle(toRecycle); + } + } + } + + @Override public Buffer write(ByteString byteString) { + if (byteString == null) throw new IllegalArgumentException("byteString == null"); + return write(byteString.data, 0, byteString.data.length); + } + + @Override public Buffer writeUtf8(String string) { + if (string == null) throw new IllegalArgumentException("string == null"); + // TODO: inline UTF-8 encoding to save allocating a byte[]? + return writeString(string, Util.UTF_8); + } + + @Override public Buffer writeString(String string, Charset charset) { + if (string == null) throw new IllegalArgumentException("string == null"); + if (charset == null) throw new IllegalArgumentException("charset == null"); + byte[] data = string.getBytes(charset); + return write(data, 0, data.length); + } + + @Override public Buffer write(byte[] source) { + if (source == null) throw new IllegalArgumentException("source == null"); + return write(source, 0, source.length); + } + + @Override public Buffer write(byte[] source, int offset, int byteCount) { + if (source == null) throw new IllegalArgumentException("source == null"); + checkOffsetAndCount(source.length, offset, byteCount); + + int limit = offset + byteCount; + while (offset < limit) { + Segment tail = writableSegment(1); + + int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit); + System.arraycopy(source, offset, tail.data, tail.limit, toCopy); + + offset += toCopy; + tail.limit += toCopy; + } + + this.size += byteCount; + return this; + } + + @Override public long writeAll(Source source) throws IOException { + if (source == null) throw new IllegalArgumentException("source == null"); + long totalBytesRead = 0; + for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) { + totalBytesRead += readCount; + } + return totalBytesRead; + } + + @Override public Buffer writeByte(int b) { + Segment tail = writableSegment(1); + tail.data[tail.limit++] = (byte) b; + size += 1; + return this; + } + + @Override public Buffer writeShort(int s) { + Segment tail = writableSegment(2); + byte[] data = tail.data; + int limit = tail.limit; + data[limit++] = (byte) ((s >>> 8) & 0xff); + data[limit++] = (byte) (s & 0xff); + tail.limit = limit; + size += 2; + return this; + } + + @Override public Buffer writeShortLe(int s) { + return writeShort(Util.reverseBytesShort((short) s)); + } + + @Override public Buffer writeInt(int i) { + Segment tail = writableSegment(4); + byte[] data = tail.data; + int limit = tail.limit; + data[limit++] = (byte) ((i >>> 24) & 0xff); + data[limit++] = (byte) ((i >>> 16) & 0xff); + data[limit++] = (byte) ((i >>> 8) & 0xff); + data[limit++] = (byte) (i & 0xff); + tail.limit = limit; + size += 4; + return this; + } + + @Override public Buffer writeIntLe(int i) { + return writeInt(Util.reverseBytesInt(i)); + } + + @Override public Buffer writeLong(long v) { + Segment tail = writableSegment(8); + byte[] data = tail.data; + int limit = tail.limit; + data[limit++] = (byte) ((v >>> 56L) & 0xff); + data[limit++] = (byte) ((v >>> 48L) & 0xff); + data[limit++] = (byte) ((v >>> 40L) & 0xff); + data[limit++] = (byte) ((v >>> 32L) & 0xff); + data[limit++] = (byte) ((v >>> 24L) & 0xff); + data[limit++] = (byte) ((v >>> 16L) & 0xff); + data[limit++] = (byte) ((v >>> 8L) & 0xff); + data[limit++] = (byte) (v & 0xff); + tail.limit = limit; + size += 8; + return this; + } + + @Override public Buffer writeLongLe(long v) { + return writeLong(reverseBytesLong(v)); + } + + /** + * Returns a tail segment that we can write at least {@code minimumCapacity} + * bytes to, creating it if necessary. + */ + Segment writableSegment(int minimumCapacity) { + if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException(); + + if (head == null) { + head = SegmentPool.getInstance().take(); // Acquire a first segment. + return head.next = head.prev = head; + } + + Segment tail = head.prev; + if (tail.limit + minimumCapacity > Segment.SIZE) { + tail = tail.push(SegmentPool.getInstance().take()); // Append a new empty segment to fill up. + } + return tail; + } + + @Override public void write(Buffer source, long byteCount) { + // Move bytes from the head of the source buffer to the tail of this buffer + // while balancing two conflicting goals: don't waste CPU and don't waste + // memory. + // + // + // Don't waste CPU (ie. don't copy data around). + // + // Copying large amounts of data is expensive. Instead, we prefer to + // reassign entire segments from one buffer to the other. + // + // + // Don't waste memory. + // + // As an invariant, adjacent pairs of segments in a buffer should be at + // least 50% full, except for the head segment and the tail segment. + // + // The head segment cannot maintain the invariant because the application is + // consuming bytes from this segment, decreasing its level. + // + // The tail segment cannot maintain the invariant because the application is + // producing bytes, which may require new nearly-empty tail segments to be + // appended. + // + // + // Moving segments between buffers + // + // When writing one buffer to another, we prefer to reassign entire segments + // over copying bytes into their most compact form. Suppose we have a buffer + // with these segment levels [91%, 61%]. If we append a buffer with a + // single [72%] segment, that yields [91%, 61%, 72%]. No bytes are copied. + // + // Or suppose we have a buffer with these segment levels: [100%, 2%], and we + // want to append it to a buffer with these segment levels [99%, 3%]. This + // operation will yield the following segments: [100%, 2%, 99%, 3%]. That + // is, we do not spend time copying bytes around to achieve more efficient + // memory use like [100%, 100%, 4%]. + // + // When combining buffers, we will compact adjacent buffers when their + // combined level doesn't exceed 100%. For example, when we start with + // [100%, 40%] and append [30%, 80%], the result is [100%, 70%, 80%]. + // + // + // Splitting segments + // + // Occasionally we write only part of a source buffer to a sink buffer. For + // example, given a sink [51%, 91%], we may want to write the first 30% of + // a source [92%, 82%] to it. To simplify, we first transform the source to + // an equivalent buffer [30%, 62%, 82%] and then move the head segment, + // yielding sink [51%, 91%, 30%] and source [62%, 82%]. + + if (source == null) throw new IllegalArgumentException("source == null"); + if (source == this) throw new IllegalArgumentException("source == this"); + checkOffsetAndCount(source.size, 0, byteCount); + + while (byteCount > 0) { + // Is a prefix of the source's head segment all that we need to move? + if (byteCount < (source.head.limit - source.head.pos)) { + Segment tail = head != null ? head.prev : null; + if (tail == null || byteCount + (tail.limit - tail.pos) > Segment.SIZE) { + // We're going to need another segment. Split the source's head + // segment in two, then move the first of those two to this buffer. + source.head = source.head.split((int) byteCount); + } else { + // Our existing segments are sufficient. Move bytes from source's head to our tail. + source.head.writeTo(tail, (int) byteCount); + source.size -= byteCount; + this.size += byteCount; + return; + } + } + + // Remove the source's head segment and append it to our tail. + Segment segmentToMove = source.head; + long movedByteCount = segmentToMove.limit - segmentToMove.pos; + source.head = segmentToMove.pop(); + if (head == null) { + head = segmentToMove; + head.next = head.prev = head; + } else { + Segment tail = head.prev; + tail = tail.push(segmentToMove); + tail.compact(); + } + source.size -= movedByteCount; + this.size += movedByteCount; + byteCount -= movedByteCount; + } + } + + @Override public long read(Buffer sink, long byteCount) { + if (sink == null) throw new IllegalArgumentException("sink == null"); + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (this.size == 0) return -1L; + if (byteCount > this.size) byteCount = this.size; + sink.write(this, byteCount); + return byteCount; + } + + @Override public long indexOf(byte b) { + return indexOf(b, 0); + } + + /** + * Returns the index of {@code b} in this at or beyond {@code fromIndex}, or + * -1 if this buffer does not contain {@code b} in that range. + */ + public long indexOf(byte b, long fromIndex) { + if (fromIndex < 0) throw new IllegalArgumentException("fromIndex < 0"); + + Segment s = head; + if (s == null) return -1L; + long offset = 0L; + do { + int segmentByteCount = s.limit - s.pos; + if (fromIndex >= segmentByteCount) { + fromIndex -= segmentByteCount; + } else { + byte[] data = s.data; + for (long pos = s.pos + fromIndex, limit = s.limit; pos < limit; pos++) { + if (data[(int) pos] == b) return offset + pos - s.pos; + } + fromIndex = 0; + } + offset += segmentByteCount; + s = s.next; + } while (s != head); + return -1L; + } + + @Override public void flush() { + } + + @Override public void close() { + } + + @Override public Timeout timeout() { + return Timeout.NONE; + } + + /** For testing. This returns the sizes of the segments in this buffer. */ + List segmentSizes() { + if (head == null) return Collections.emptyList(); + List result = new ArrayList(); + result.add(head.limit - head.pos); + for (Segment s = head.next; s != head; s = s.next) { + result.add(s.limit - s.pos); + } + return result; + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Buffer)) return false; + Buffer that = (Buffer) o; + if (size != that.size) return false; + if (size == 0) return true; // Both buffers are empty. + + Segment sa = this.head; + Segment sb = that.head; + int posA = sa.pos; + int posB = sb.pos; + + for (long pos = 0, count; pos < size; pos += count) { + count = Math.min(sa.limit - posA, sb.limit - posB); + + for (int i = 0; i < count; i++) { + if (sa.data[posA++] != sb.data[posB++]) return false; + } + + if (posA == sa.limit) { + sa = sa.next; + posA = sa.pos; + } + + if (posB == sb.limit) { + sb = sb.next; + posB = sb.pos; + } + } + + return true; + } + + @Override public int hashCode() { + Segment s = head; + if (s == null) return 0; + int result = 1; + do { + for (int pos = s.pos, limit = s.limit; pos < limit; pos++) { + result = 31 * result + s.data[pos]; + } + s = s.next; + } while (s != head); + return result; + } + + @Override public String toString() { + if (size == 0) { + return "Buffer[size=0]"; + } + + if (size <= 16) { + ByteString data = clone().readByteString(); + return String.format("Buffer[size=%s data=%s]", size, data.hex()); + } + + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(head.data, head.pos, head.limit - head.pos); + for (Segment s = head.next; s != head; s = s.next) { + md5.update(s.data, s.pos, s.limit - s.pos); + } + return String.format("Buffer[size=%s md5=%s]", + size, ByteString.of(md5.digest()).hex()); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(); + } + } + + /** Returns a deep copy of this buffer. */ + @Override public Buffer clone() { + Buffer result = new Buffer(); + if (size == 0) return result; + + result.write(head.data, head.pos, head.limit - head.pos); + for (Segment s = head.next; s != head; s = s.next) { + result.write(s.data, s.pos, s.limit - s.pos); + } + + return result; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSink.java new file mode 100644 index 000000000..777840389 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSink.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +/** + * A sink that keeps a buffer internally so that callers can do small writes + * without a performance penalty. + */ +public interface BufferedSink extends Sink { + /** Returns this sink's internal buffer. */ + Buffer buffer(); + + BufferedSink write(ByteString byteString) throws IOException; + + /** + * Like {@link java.io.OutputStream#write(byte[])}, this writes a complete byte array to + * this sink. + */ + BufferedSink write(byte[] source) throws IOException; + + /** + * Like {@link java.io.OutputStream#write(byte[], int, int)}, this writes {@code byteCount} + * bytes of {@code source}, starting at {@code offset}. + */ + BufferedSink write(byte[] source, int offset, int byteCount) throws IOException; + + /** + * Removes all bytes from {@code source} and appends them to this. Returns the + * number of bytes read which will be 0 if {@code source} is exhausted. + */ + long writeAll(Source source) throws IOException; + + /** Encodes {@code string} in UTF-8 and writes it to this sink. */ + BufferedSink writeUtf8(String string) throws IOException; + + /** Encodes {@code string} in {@code charset} and writes it to this sink. */ + BufferedSink writeString(String string, Charset charset) throws IOException; + + /** Writes a byte to this sink. */ + BufferedSink writeByte(int b) throws IOException; + + /** Writes a big-endian short to this sink using two bytes. */ + BufferedSink writeShort(int s) throws IOException; + + /** Writes a little-endian short to this sink using two bytes. */ + BufferedSink writeShortLe(int s) throws IOException; + + /** Writes a big-endian int to this sink using four bytes. */ + BufferedSink writeInt(int i) throws IOException; + + /** Writes a little-endian int to this sink using four bytes. */ + BufferedSink writeIntLe(int i) throws IOException; + + /** Writes a big-endian long to this sink using eight bytes. */ + BufferedSink writeLong(long v) throws IOException; + + /** Writes a little-endian long to this sink using eight bytes. */ + BufferedSink writeLongLe(long v) throws IOException; + + /** Writes complete segments to this sink. Like {@link #flush}, but weaker. */ + BufferedSink emitCompleteSegments() throws IOException; + + /** Returns an output stream that writes to this sink. */ + OutputStream outputStream(); +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSource.java new file mode 100644 index 000000000..af0129488 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSource.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +/** + * A source that keeps a buffer internally so that callers can do small reads + * without a performance penalty. It also allows clients to read ahead, + * buffering as much as necessary before consuming input. + */ +public interface BufferedSource extends Source { + /** Returns this source's internal buffer. */ + Buffer buffer(); + + /** + * Returns true if there are no more bytes in this source. This will block + * until there are bytes to read or the source is definitely exhausted. + */ + boolean exhausted() throws IOException; + + /** + * Returns when the buffer contains at least {@code byteCount} bytes. Throws + * an {@link java.io.EOFException} if the source is exhausted before the + * required bytes can be read. + */ + void require(long byteCount) throws IOException; + + /** Removes a byte from this source and returns it. */ + byte readByte() throws IOException; + + /** Removes two bytes from this source and returns a big-endian short. */ + short readShort() throws IOException; + + /** Removes two bytes from this source and returns a little-endian short. */ + short readShortLe() throws IOException; + + /** Removes four bytes from this source and returns a big-endian int. */ + int readInt() throws IOException; + + /** Removes four bytes from this source and returns a little-endian int. */ + int readIntLe() throws IOException; + + /** Removes eight bytes from this source and returns a big-endian long. */ + long readLong() throws IOException; + + /** Removes eight bytes from this source and returns a little-endian long. */ + long readLongLe() throws IOException; + + /** + * Reads and discards {@code byteCount} bytes from this source. Throws an + * {@link java.io.EOFException} if the source is exhausted before the + * requested bytes can be skipped. + */ + void skip(long byteCount) throws IOException; + + /** Removes all bytes bytes from this and returns them as a byte string. */ + ByteString readByteString() throws IOException; + + /** Removes {@code byteCount} bytes from this and returns them as a byte string. */ + ByteString readByteString(long byteCount) throws IOException; + + /** Removes all bytes from this and returns them as a byte array. */ + byte[] readByteArray() throws IOException; + + /** Removes {@code byteCount} bytes from this and returns them as a byte array. */ + byte[] readByteArray(long byteCount) throws IOException; + + /** + * Removes up to {@code sink.length} bytes from this and copies them into {@code sink}. + * Returns the number of bytes read, or -1 if this source is exhausted. + */ + int read(byte[] sink) throws IOException; + + /** + * Removes exactly {@code sink.length} bytes from this and copies them into {@code sink}. + * Throws an {@link java.io.EOFException} if the requested number of bytes cannot be read. + */ + void readFully(byte[] sink) throws IOException; + + /** + * Removes up to {@code byteCount} bytes from this and copies them into {@code sink} at + * {@code offset}. Returns the number of bytes read, or -1 if this source is exhausted. + */ + int read(byte[] sink, int offset, int byteCount) throws IOException; + + /** + * Removes exactly {@code byteCount} bytes from this and appends them to + * {@code sink}. Throws an {@link java.io.EOFException} if the requested + * number of bytes cannot be read. + */ + void readFully(Buffer sink, long byteCount) throws IOException; + + /** + * Removes all bytes from this and appends them to {@code sink}. Returns the + * total number of bytes written to {@code sink} which will be 0 if this is + * exhausted. + */ + long readAll(Sink sink) throws IOException; + + /** Removes all bytes from this, decodes them as UTF-8, and returns the string. */ + String readUtf8() throws IOException; + + /** + * Removes {@code byteCount} bytes from this, decodes them as UTF-8, and + * returns the string. + */ + String readUtf8(long byteCount) throws IOException; + + /** + * Removes and returns characters up to but not including the next line break. + * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are + * not included in the result. + * + *

On the end of the stream this method returns null, just + * like {@link java.io.BufferedReader}. If the source doesn't end with a line + * break then an implicit line break is assumed. Null is returned once the + * source is exhausted. Use this for human-generated data, where a trailing + * line break is optional. + */ + String readUtf8Line() throws IOException; + + /** + * Removes and returns characters up to but not including the next line break. + * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are + * not included in the result. + * + *

On the end of the stream this method throws. Every call + * must consume either '\r\n' or '\n'. If these characters are absent in the + * stream, an {@link java.io.EOFException} is thrown. Use this for + * machine-generated data where a missing line break implies truncated input. + */ + String readUtf8LineStrict() throws IOException; + + /** + * Removes all bytes from this, decodes them as {@code charset}, and returns + * the string. + */ + String readString(Charset charset) throws IOException; + + /** + * Removes {@code byteCount} bytes from this, decodes them as {@code charset}, + * and returns the string. + */ + String readString(long byteCount, Charset charset) throws IOException; + + /** + * Returns the index of {@code b} in the buffer, refilling it if necessary + * until it is found. This reads an unbounded number of bytes into the buffer. + * Returns -1 if the stream is exhausted before the requested byte is found. + */ + long indexOf(byte b) throws IOException; + + /** Returns an input stream that reads from this source. */ + InputStream inputStream(); +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ByteString.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ByteString.java new file mode 100644 index 000000000..c029e1ee8 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ByteString.java @@ -0,0 +1,283 @@ +/* + * Copyright 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import static com.koushikdutta.async.http.spdy.okio.Util.checkOffsetAndCount; + +/** + * An immutable sequence of bytes. + * + *

Full disclosure: this class provides untrusted input and + * output streams with raw access to the underlying byte array. A hostile + * stream implementation could keep a reference to the mutable byte string, + * violating the immutable guarantee of this class. For this reason a byte + * string's immutability guarantee cannot be relied upon for security in applets + * and other environments that run both trusted and untrusted code in the same + * process. + */ +public final class ByteString implements Serializable { + private static final char[] HEX_DIGITS = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + private static final long serialVersionUID = 1L; + + /** A singleton empty {@code ByteString}. */ + public static final ByteString EMPTY = ByteString.of(); + + final byte[] data; + private transient int hashCode; // Lazily computed; 0 if unknown. + private transient String utf8; // Lazily computed. + + ByteString(byte[] data) { + this.data = data; // Trusted internal constructor doesn't clone data. + } + + /** + * Returns a new byte string containing a clone of the bytes of {@code data}. + */ + public static ByteString of(byte... data) { + if (data == null) throw new IllegalArgumentException("data == null"); + return new ByteString(data.clone()); + } + + /** + * Returns a new byte string containing a copy of {@code byteCount} bytes of {@code data} starting + * at {@code offset}. + */ + public static ByteString of(byte[] data, int offset, int byteCount) { + if (data == null) throw new IllegalArgumentException("data == null"); + checkOffsetAndCount(data.length, offset, byteCount); + + byte[] copy = new byte[byteCount]; + System.arraycopy(data, offset, copy, 0, byteCount); + return new ByteString(copy); + } + + /** Returns a new byte string containing the {@code UTF-8} bytes of {@code s}. */ + public static ByteString encodeUtf8(String s) { + if (s == null) throw new IllegalArgumentException("s == null"); + ByteString byteString = new ByteString(s.getBytes(Util.UTF_8)); + byteString.utf8 = s; + return byteString; + } + + /** Constructs a new {@code String} by decoding the bytes as {@code UTF-8}. */ + public String utf8() { + String result = utf8; + // We don't care if we double-allocate in racy code. + return result != null ? result : (utf8 = new String(data, Util.UTF_8)); + } + + /** + * Returns this byte string encoded as Base64. In violation of the + * RFC, the returned string does not wrap lines at 76 columns. + */ + public String base64() { + return Base64.encode(data); + } + + /** + * Decodes the Base64-encoded bytes and returns their value as a byte string. + * Returns null if {@code base64} is not a Base64-encoded sequence of bytes. + */ + public static ByteString decodeBase64(String base64) { + if (base64 == null) throw new IllegalArgumentException("base64 == null"); + byte[] decoded = Base64.decode(base64); + return decoded != null ? new ByteString(decoded) : null; + } + + /** Returns this byte string encoded in hexadecimal. */ + public String hex() { + char[] result = new char[data.length * 2]; + int c = 0; + for (byte b : data) { + result[c++] = HEX_DIGITS[(b >> 4) & 0xf]; + result[c++] = HEX_DIGITS[b & 0xf]; + } + return new String(result); + } + + /** Decodes the hex-encoded bytes and returns their value a byte string. */ + public static ByteString decodeHex(String hex) { + if (hex == null) throw new IllegalArgumentException("hex == null"); + if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex); + + byte[] result = new byte[hex.length() / 2]; + for (int i = 0; i < result.length; i++) { + int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4; + int d2 = decodeHexDigit(hex.charAt(i * 2 + 1)); + result[i] = (byte) (d1 + d2); + } + return of(result); + } + + private static int decodeHexDigit(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + throw new IllegalArgumentException("Unexpected hex digit: " + c); + } + + /** + * Reads {@code count} bytes from {@code in} and returns the result. + * + * @throws java.io.EOFException if {@code in} has fewer than {@code count} + * bytes to read. + */ + public static ByteString read(InputStream in, int byteCount) throws IOException { + if (in == null) throw new IllegalArgumentException("in == null"); + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + + byte[] result = new byte[byteCount]; + for (int offset = 0, read; offset < byteCount; offset += read) { + read = in.read(result, offset, byteCount - offset); + if (read == -1) throw new EOFException(); + } + return new ByteString(result); + } + + /** + * Returns a byte string equal to this byte string, but with the bytes 'A' + * through 'Z' replaced with the corresponding byte in 'a' through 'z'. + * Returns this byte string if it contains no bytes in 'A' through 'Z'. + */ + public ByteString toAsciiLowercase() { + // Search for an uppercase character. If we don't find one, return this. + for (int i = 0; i < data.length; i++) { + byte c = data[i]; + if (c < 'A' || c > 'Z') continue; + + // If we reach this point, this string is not not lowercase. Create and + // return a new byte string. + byte[] lowercase = data.clone(); + lowercase[i++] = (byte) (c - ('A' - 'a')); + for (; i < lowercase.length; i++) { + c = lowercase[i]; + if (c < 'A' || c > 'Z') continue; + lowercase[i] = (byte) (c - ('A' - 'a')); + } + return new ByteString(lowercase); + } + return this; + } + + /** + * Returns a byte string equal to this byte string, but with the bytes 'a' + * through 'z' replaced with the corresponding byte in 'A' through 'Z'. + * Returns this byte string if it contains no bytes in 'a' through 'z'. + */ + public ByteString toAsciiUppercase() { + // Search for an lowercase character. If we don't find one, return this. + for (int i = 0; i < data.length; i++) { + byte c = data[i]; + if (c < 'a' || c > 'z') continue; + + // If we reach this point, this string is not not uppercase. Create and + // return a new byte string. + byte[] lowercase = data.clone(); + lowercase[i++] = (byte) (c - ('a' - 'A')); + for (; i < lowercase.length; i++) { + c = lowercase[i]; + if (c < 'a' || c > 'z') continue; + lowercase[i] = (byte) (c - ('a' - 'A')); + } + return new ByteString(lowercase); + } + return this; + } + + /** Returns the byte at {@code pos}. */ + public byte getByte(int pos) { + return data[pos]; + } + + /** + * Returns the number of bytes in this ByteString. + */ + public int size() { + return data.length; + } + + /** + * Returns a byte array containing a copy of the bytes in this {@code ByteString}. + */ + public byte[] toByteArray() { + return data.clone(); + } + + /** Writes the contents of this byte string to {@code out}. */ + public void write(OutputStream out) throws IOException { + if (out == null) throw new IllegalArgumentException("out == null"); + out.write(data); + } + + @Override public boolean equals(Object o) { + return o == this || o instanceof ByteString && Arrays.equals(((ByteString) o).data, data); + } + + @Override public int hashCode() { + int result = hashCode; + return result != 0 ? result : (hashCode = Arrays.hashCode(data)); + } + + @Override public String toString() { + if (data.length == 0) { + return "ByteString[size=0]"; + } + + if (data.length <= 16) { + return String.format("ByteString[size=%s data=%s]", data.length, hex()); + } + + try { + return String.format("ByteString[size=%s md5=%s]", data.length, + ByteString.of(MessageDigest.getInstance("MD5").digest(data)).hex()); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(); + } + } + + private void readObject(ObjectInputStream in) throws IOException { + int dataLength = in.readInt(); + ByteString byteString = ByteString.read(in, dataLength); + try { + Field field = ByteString.class.getDeclaredField("data"); + field.setAccessible(true); + field.set(this, byteString.data); + } catch (NoSuchFieldException e) { + throw new AssertionError(); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(data.length); + out.write(data); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/DeflaterSink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/DeflaterSink.java new file mode 100644 index 000000000..960ee80c3 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/DeflaterSink.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.IOException; +import java.util.zip.Deflater; + +import static com.koushikdutta.async.http.spdy.okio.Util.checkOffsetAndCount; + +/** + * A sink that uses DEFLATE to + * compress data written to another source. + * + *

Sync flush

+ * Aggressive flushing of this stream may result in reduced compression. Each + * call to {@link #flush} immediately compresses all currently-buffered data; + * this early compression may be less effective than compression performed + * without flushing. + * + *

This is equivalent to using {@link java.util.zip.Deflater} with the sync flush option. + * This class does not offer any partial flush mechanism. For best performance, + * only call {@link #flush} when application behavior requires it. + */ +public final class DeflaterSink implements Sink { + private final BufferedSink sink; + private final Deflater deflater; + private boolean closed; + + public DeflaterSink(Sink sink, Deflater deflater) { + this(Okio.buffer(sink), deflater); + } + + /** + * This package-private constructor shares a buffer with its trusted caller. + * In general we can't share a BufferedSource because the deflater holds input + * bytes until they are inflated. + */ + DeflaterSink(BufferedSink sink, Deflater deflater) { + if (sink == null) throw new IllegalArgumentException("source == null"); + if (deflater == null) throw new IllegalArgumentException("inflater == null"); + this.sink = sink; + this.deflater = deflater; + } + + @Override public void write(Buffer source, long byteCount) + throws IOException { + checkOffsetAndCount(source.size, 0, byteCount); + while (byteCount > 0) { + // Share bytes from the head segment of 'source' with the deflater. + Segment head = source.head; + int toDeflate = (int) Math.min(byteCount, head.limit - head.pos); + deflater.setInput(head.data, head.pos, toDeflate); + + // Deflate those bytes into sink. + deflate(false); + + // Mark those bytes as read. + source.size -= toDeflate; + head.pos += toDeflate; + if (head.pos == head.limit) { + source.head = head.pop(); + SegmentPool.getInstance().recycle(head); + } + + byteCount -= toDeflate; + } + } + + private void deflate(boolean syncFlush) throws IOException { + Buffer buffer = sink.buffer(); + while (true) { + Segment s = buffer.writableSegment(1); + + // The 4-parameter overload of deflate() doesn't exist in the RI until + // Java 1.7, and is public (although with @hide) on Android since 2.3. + // The @hide tag means that this code won't compile against the Android + // 2.3 SDK, but it will run fine there. + int deflated = syncFlush + ? deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit, Deflater.SYNC_FLUSH) + : deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit); + + if (deflated > 0) { + s.limit += deflated; + buffer.size += deflated; + sink.emitCompleteSegments(); + } else if (deflater.needsInput()) { + return; + } + } + } + + @Override public void flush() throws IOException { + deflate(true); + sink.flush(); + } + + void finishDeflate() throws IOException { + deflater.finish(); + deflate(false); + } + + @Override public void close() throws IOException { + if (closed) return; + + // Emit deflated data to the underlying sink. If this fails, we still need + // to close the deflater and the sink; otherwise we risk leaking resources. + Throwable thrown = null; + try { + finishDeflate(); + } catch (Throwable e) { + thrown = e; + } + + try { + deflater.end(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + + try { + sink.close(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + closed = true; + + if (thrown != null) Util.sneakyRethrow(thrown); + } + + @Override public Timeout timeout() { + return sink.timeout(); + } + + @Override public String toString() { + return "DeflaterSink(" + sink + ")"; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ForwardingSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ForwardingSource.java new file mode 100644 index 000000000..5e48dfd54 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ForwardingSource.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.IOException; + +/** A {@link Source} which forwards calls to another. Useful for subclassing. */ +public abstract class ForwardingSource implements Source { + private final Source delegate; + + public ForwardingSource(Source delegate) { + if (delegate == null) throw new IllegalArgumentException("delegate == null"); + this.delegate = delegate; + } + + /** {@link Source} to which this instance is delegating. */ + public final Source delegate() { + return delegate; + } + + @Override public long read(Buffer sink, long byteCount) throws IOException { + return delegate.read(sink, byteCount); + } + + @Override public Timeout timeout() { + return delegate.timeout(); + } + + @Override public void close() throws IOException { + delegate.close(); + } + + @Override public String toString() { + return getClass().getSimpleName() + "(" + delegate.toString() + ")"; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java new file mode 100644 index 000000000..ce50796f3 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.EOFException; +import java.io.IOException; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * A source that uses DEFLATE + * to decompress data read from another source. + */ +public final class InflaterSource implements Source { + private final BufferedSource source; + private final Inflater inflater; + + /** + * When we call Inflater.setInput(), the inflater keeps our byte array until + * it needs input again. This tracks how many bytes the inflater is currently + * holding on to. + */ + private int bufferBytesHeldByInflater; + private boolean closed; + + public InflaterSource(Source source, Inflater inflater) { + this(Okio.buffer(source), inflater); + } + + /** + * This package-private constructor shares a buffer with its trusted caller. + * In general we can't share a BufferedSource because the inflater holds input + * bytes until they are inflated. + */ + InflaterSource(BufferedSource source, Inflater inflater) { + if (source == null) throw new IllegalArgumentException("source == null"); + if (inflater == null) throw new IllegalArgumentException("inflater == null"); + this.source = source; + this.inflater = inflater; + } + + @Override public long read( + Buffer sink, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + if (byteCount == 0) return 0; + + while (true) { + boolean sourceExhausted = refill(); + + // Decompress the inflater's compressed data into the sink. + try { + Segment tail = sink.writableSegment(1); + int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit); + if (bytesInflated > 0) { + tail.limit += bytesInflated; + sink.size += bytesInflated; + return bytesInflated; + } + if (inflater.finished() || inflater.needsDictionary()) { + releaseInflatedBytes(); + return -1; + } + if (sourceExhausted) throw new EOFException("source exhausted prematurely"); + } catch (DataFormatException e) { + throw new IOException(e); + } + } + } + + /** + * Refills the inflater with compressed data if it needs input. (And only if + * it needs input). Returns true if the inflater required input but the source + * was exhausted. + */ + public boolean refill() throws IOException { + if (!inflater.needsInput()) return false; + + releaseInflatedBytes(); + if (inflater.getRemaining() != 0) throw new IllegalStateException("?"); // TODO: possible? + + // If there are compressed bytes in the source, assign them to the inflater. + if (source.exhausted()) return true; + + // Assign buffer bytes to the inflater. + byte[] data = source.readByteArray(); + bufferBytesHeldByInflater = data.length; + inflater.setInput(data, 0, bufferBytesHeldByInflater); + return false; + } + + /** When the inflater has processed compressed data, remove it from the buffer. */ + private void releaseInflatedBytes() throws IOException { + if (bufferBytesHeldByInflater == 0) return; + int toRelease = bufferBytesHeldByInflater - inflater.getRemaining(); + bufferBytesHeldByInflater -= toRelease; + source.skip(toRelease); + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public void close() throws IOException { + if (closed) return; + inflater.end(); + closed = true; + source.close(); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Okio.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Okio.java new file mode 100644 index 000000000..4aa579443 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Okio.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static com.koushikdutta.async.http.spdy.okio.Util.checkOffsetAndCount; + +/** Essential APIs for working with Okio. */ +public final class Okio { + private static final Logger logger = Logger.getLogger(Okio.class.getName()); + + private Okio() { + } + + /** + * Returns a new source that buffers reads from {@code source}. The returned + * source will perform bulk reads into its in-memory buffer. Use this wherever + * you read a source to get an ergonomic and efficient access to data. + */ + public static BufferedSource buffer(Source source) { + if (source == null) throw new IllegalArgumentException("source == null"); + return new RealBufferedSource(source); + } + + /** + * Returns a new sink that buffers writes to {@code sink}. The returned sink + * will batch writes to {@code sink}. Use this wherever you write to a sink to + * get an ergonomic and efficient access to data. + */ + public static BufferedSink buffer(Sink sink) { + if (sink == null) throw new IllegalArgumentException("sink == null"); + return new RealBufferedSink(sink); + } + + /** Returns a sink that writes to {@code out}. */ + public static Sink sink(final OutputStream out) { + return sink(out, new Timeout()); + } + + private static Sink sink(final OutputStream out, final Timeout timeout) { + if (out == null) throw new IllegalArgumentException("out == null"); + if (timeout == null) throw new IllegalArgumentException("timeout == null"); + + return new Sink() { + @Override public void write(Buffer source, long byteCount) throws IOException { + checkOffsetAndCount(source.size, 0, byteCount); + while (byteCount > 0) { + timeout.throwIfReached(); + Segment head = source.head; + int toCopy = (int) Math.min(byteCount, head.limit - head.pos); + out.write(head.data, head.pos, toCopy); + + head.pos += toCopy; + byteCount -= toCopy; + source.size -= toCopy; + + if (head.pos == head.limit) { + source.head = head.pop(); + SegmentPool.getInstance().recycle(head); + } + } + } + + @Override public void flush() throws IOException { + out.flush(); + } + + @Override public void close() throws IOException { + out.close(); + } + + @Override public Timeout timeout() { + return timeout; + } + + @Override public String toString() { + return "sink(" + out + ")"; + } + }; + } + + /** + * Returns a sink that writes to {@code socket}. Prefer this over {@link + * #sink(java.io.OutputStream)} because this method honors timeouts. When the socket + * write times out, the socket is asynchronously closed by a watchdog thread. + */ + public static Sink sink(final Socket socket) throws IOException { + if (socket == null) throw new IllegalArgumentException("socket == null"); + AsyncTimeout timeout = timeout(socket); + Sink sink = sink(socket.getOutputStream(), timeout); + return timeout.sink(sink); + } + + /** Returns a source that reads from {@code in}. */ + public static Source source(final InputStream in) { + return source(in, new Timeout()); + } + + private static Source source(final InputStream in, final Timeout timeout) { + if (in == null) throw new IllegalArgumentException("in == null"); + if (timeout == null) throw new IllegalArgumentException("timeout == null"); + + return new Source() { + @Override public long read(Buffer sink, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + timeout.throwIfReached(); + Segment tail = sink.writableSegment(1); + int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit); + int bytesRead = in.read(tail.data, tail.limit, maxToCopy); + if (bytesRead == -1) return -1; + tail.limit += bytesRead; + sink.size += bytesRead; + return bytesRead; + } + + @Override public void close() throws IOException { + in.close(); + } + + @Override public Timeout timeout() { + return timeout; + } + + @Override public String toString() { + return "source(" + in + ")"; + } + }; + } + + /** Returns a source that reads from {@code file}. */ + public static Source source(File file) throws FileNotFoundException { + if (file == null) throw new IllegalArgumentException("file == null"); + return source(new FileInputStream(file)); + } + + /** Returns a sink that writes to {@code file}. */ + public static Sink sink(File file) throws FileNotFoundException { + if (file == null) throw new IllegalArgumentException("file == null"); + return sink(new FileOutputStream(file)); + } + + /** Returns a sink that appends to {@code file}. */ + public static Sink appendingSink(File file) throws FileNotFoundException { + if (file == null) throw new IllegalArgumentException("file == null"); + return sink(new FileOutputStream(file, true)); + } + + /** + * Returns a source that reads from {@code socket}. Prefer this over {@link + * #source(java.io.InputStream)} because this method honors timeouts. When the socket + * read times out, the socket is asynchronously closed by a watchdog thread. + */ + public static Source source(final Socket socket) throws IOException { + if (socket == null) throw new IllegalArgumentException("socket == null"); + AsyncTimeout timeout = timeout(socket); + Source source = source(socket.getInputStream(), timeout); + return timeout.source(source); + } + + private static AsyncTimeout timeout(final Socket socket) { + return new AsyncTimeout() { + @Override protected void timedOut() { + try { + socket.close(); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e); + } + } + }; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSink.java new file mode 100644 index 000000000..8e393ca42 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSink.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +final class RealBufferedSink implements BufferedSink { + public final Buffer buffer; + public final Sink sink; + private boolean closed; + + public RealBufferedSink(Sink sink, Buffer buffer) { + if (sink == null) throw new IllegalArgumentException("sink == null"); + this.buffer = buffer; + this.sink = sink; + } + + public RealBufferedSink(Sink sink) { + this(sink, new Buffer()); + } + + @Override public Buffer buffer() { + return buffer; + } + + @Override public void write(Buffer source, long byteCount) + throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.write(source, byteCount); + emitCompleteSegments(); + } + + @Override public BufferedSink write(ByteString byteString) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.write(byteString); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeUtf8(String string) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeUtf8(string); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeString(String string, Charset charset) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeString(string, charset); + return emitCompleteSegments(); + } + + @Override public BufferedSink write(byte[] source) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.write(source); + return emitCompleteSegments(); + } + + @Override public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.write(source, offset, byteCount); + return emitCompleteSegments(); + } + + @Override public long writeAll(Source source) throws IOException { + if (source == null) throw new IllegalArgumentException("source == null"); + long totalBytesRead = 0; + for (long readCount; (readCount = source.read(buffer, Segment.SIZE)) != -1; ) { + totalBytesRead += readCount; + emitCompleteSegments(); + } + return totalBytesRead; + } + + @Override public BufferedSink writeByte(int b) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeByte(b); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeShort(int s) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeShort(s); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeShortLe(int s) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeShortLe(s); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeInt(int i) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeInt(i); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeIntLe(int i) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeIntLe(i); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeLong(long v) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeLong(v); + return emitCompleteSegments(); + } + + @Override public BufferedSink writeLongLe(long v) throws IOException { + if (closed) throw new IllegalStateException("closed"); + buffer.writeLongLe(v); + return emitCompleteSegments(); + } + + @Override public BufferedSink emitCompleteSegments() throws IOException { + if (closed) throw new IllegalStateException("closed"); + long byteCount = buffer.completeSegmentByteCount(); + if (byteCount > 0) sink.write(buffer, byteCount); + return this; + } + + @Override public OutputStream outputStream() { + return new OutputStream() { + @Override public void write(int b) throws IOException { + if (closed) throw new IOException("closed"); + buffer.writeByte((byte) b); + emitCompleteSegments(); + } + + @Override public void write(byte[] data, int offset, int byteCount) throws IOException { + if (closed) throw new IOException("closed"); + buffer.write(data, offset, byteCount); + emitCompleteSegments(); + } + + @Override public void flush() throws IOException { + // For backwards compatibility, a flush() on a closed stream is a no-op. + if (!closed) { + RealBufferedSink.this.flush(); + } + } + + @Override public void close() throws IOException { + RealBufferedSink.this.close(); + } + + @Override public String toString() { + return RealBufferedSink.this + ".outputStream()"; + } + }; + } + + @Override public void flush() throws IOException { + if (closed) throw new IllegalStateException("closed"); + if (buffer.size > 0) { + sink.write(buffer, buffer.size); + } + sink.flush(); + } + + @Override public void close() throws IOException { + if (closed) return; + + // Emit buffered data to the underlying sink. If this fails, we still need + // to close the sink; otherwise we risk leaking resources. + Throwable thrown = null; + try { + if (buffer.size > 0) { + sink.write(buffer, buffer.size); + } + } catch (Throwable e) { + thrown = e; + } + + try { + sink.close(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + closed = true; + + if (thrown != null) Util.sneakyRethrow(thrown); + } + + @Override public Timeout timeout() { + return sink.timeout(); + } + + @Override public String toString() { + return "buffer(" + sink + ")"; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSource.java new file mode 100644 index 000000000..0397efdd0 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSource.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; + +import static com.koushikdutta.async.http.spdy.okio.Util.checkOffsetAndCount; + +final class RealBufferedSource implements BufferedSource { + public final Buffer buffer; + public final Source source; + private boolean closed; + + public RealBufferedSource(Source source, Buffer buffer) { + if (source == null) throw new IllegalArgumentException("source == null"); + this.buffer = buffer; + this.source = source; + } + + public RealBufferedSource(Source source) { + this(source, new Buffer()); + } + + @Override public Buffer buffer() { + return buffer; + } + + @Override public long read(Buffer sink, long byteCount) throws IOException { + if (sink == null) throw new IllegalArgumentException("sink == null"); + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + + if (buffer.size == 0) { + long read = source.read(buffer, Segment.SIZE); + if (read == -1) return -1; + } + + long toRead = Math.min(byteCount, buffer.size); + return buffer.read(sink, toRead); + } + + @Override public boolean exhausted() throws IOException { + if (closed) throw new IllegalStateException("closed"); + return buffer.exhausted() && source.read(buffer, Segment.SIZE) == -1; + } + + @Override public void require(long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + while (buffer.size < byteCount) { + if (source.read(buffer, Segment.SIZE) == -1) throw new EOFException(); + } + } + + @Override public byte readByte() throws IOException { + require(1); + return buffer.readByte(); + } + + @Override public ByteString readByteString() throws IOException { + buffer.writeAll(source); + return buffer.readByteString(); + } + + @Override public ByteString readByteString(long byteCount) throws IOException { + require(byteCount); + return buffer.readByteString(byteCount); + } + + @Override public byte[] readByteArray() throws IOException { + buffer.writeAll(source); + return buffer.readByteArray(); + } + + @Override public byte[] readByteArray(long byteCount) throws IOException { + require(byteCount); + return buffer.readByteArray(byteCount); + } + + @Override public int read(byte[] sink) throws IOException { + return read(sink, 0, sink.length); + } + + @Override public void readFully(byte[] sink) throws IOException { + try { + require(sink.length); + } catch (EOFException e) { + // The underlying source is exhausted. Copy the bytes we got before rethrowing. + int offset = 0; + while (buffer.size > 0) { + int read = buffer.read(sink, offset, (int) buffer.size - offset); + if (read == -1) throw new AssertionError(); + offset += read; + } + throw e; + } + buffer.readFully(sink); + } + + @Override public int read(byte[] sink, int offset, int byteCount) throws IOException { + checkOffsetAndCount(sink.length, offset, byteCount); + + if (buffer.size == 0) { + long read = source.read(buffer, Segment.SIZE); + if (read == -1) return -1; + } + + int toRead = (int) Math.min(byteCount, buffer.size); + return buffer.read(sink, offset, toRead); + } + + @Override public void readFully(Buffer sink, long byteCount) throws IOException { + try { + require(byteCount); + } catch (EOFException e) { + // The underlying source is exhausted. Copy the bytes we got before rethrowing. + sink.writeAll(buffer); + throw e; + } + buffer.readFully(sink, byteCount); + } + + @Override public long readAll(Sink sink) throws IOException { + if (sink == null) throw new IllegalArgumentException("sink == null"); + + long totalBytesWritten = 0; + while (source.read(buffer, Segment.SIZE) != -1) { + long emitByteCount = buffer.completeSegmentByteCount(); + if (emitByteCount > 0) { + totalBytesWritten += emitByteCount; + sink.write(buffer, emitByteCount); + } + } + if (buffer.size() > 0) { + totalBytesWritten += buffer.size(); + sink.write(buffer, buffer.size()); + } + return totalBytesWritten; + } + + @Override public String readUtf8() throws IOException { + buffer.writeAll(source); + return buffer.readUtf8(); + } + + @Override public String readUtf8(long byteCount) throws IOException { + require(byteCount); + return buffer.readUtf8(byteCount); + } + + @Override public String readString(Charset charset) throws IOException { + if (charset == null) throw new IllegalArgumentException("charset == null"); + + buffer.writeAll(source); + return buffer.readString(charset); + } + + @Override public String readString(long byteCount, Charset charset) throws IOException { + require(byteCount); + if (charset == null) throw new IllegalArgumentException("charset == null"); + return buffer.readString(byteCount, charset); + } + + @Override public String readUtf8Line() throws IOException { + long newline = indexOf((byte) '\n'); + + if (newline == -1) { + return buffer.size != 0 ? readUtf8(buffer.size) : null; + } + + return buffer.readUtf8Line(newline); + } + + @Override public String readUtf8LineStrict() throws IOException { + long newline = indexOf((byte) '\n'); + if (newline == -1L) throw new EOFException(); + return buffer.readUtf8Line(newline); + } + + @Override public short readShort() throws IOException { + require(2); + return buffer.readShort(); + } + + @Override public short readShortLe() throws IOException { + require(2); + return buffer.readShortLe(); + } + + @Override public int readInt() throws IOException { + require(4); + return buffer.readInt(); + } + + @Override public int readIntLe() throws IOException { + require(4); + return buffer.readIntLe(); + } + + @Override public long readLong() throws IOException { + require(8); + return buffer.readLong(); + } + + @Override public long readLongLe() throws IOException { + require(8); + return buffer.readLongLe(); + } + + @Override public void skip(long byteCount) throws IOException { + if (closed) throw new IllegalStateException("closed"); + while (byteCount > 0) { + if (buffer.size == 0 && source.read(buffer, Segment.SIZE) == -1) { + throw new EOFException(); + } + long toSkip = Math.min(byteCount, buffer.size()); + buffer.skip(toSkip); + byteCount -= toSkip; + } + } + + @Override public long indexOf(byte b) throws IOException { + if (closed) throw new IllegalStateException("closed"); + long start = 0; + long index; + while ((index = buffer.indexOf(b, start)) == -1) { + start = buffer.size; + if (source.read(buffer, Segment.SIZE) == -1) return -1L; + } + return index; + } + + @Override public InputStream inputStream() { + return new InputStream() { + @Override public int read() throws IOException { + if (closed) throw new IOException("closed"); + if (buffer.size == 0) { + long count = source.read(buffer, Segment.SIZE); + if (count == -1) return -1; + } + return buffer.readByte() & 0xff; + } + + @Override public int read(byte[] data, int offset, int byteCount) throws IOException { + if (closed) throw new IOException("closed"); + checkOffsetAndCount(data.length, offset, byteCount); + + if (buffer.size == 0) { + long count = source.read(buffer, Segment.SIZE); + if (count == -1) return -1; + } + + return buffer.read(data, offset, byteCount); + } + + @Override public int available() throws IOException { + if (closed) throw new IOException("closed"); + return (int) Math.min(buffer.size, Integer.MAX_VALUE); + } + + @Override public void close() throws IOException { + RealBufferedSource.this.close(); + } + + @Override public String toString() { + return RealBufferedSource.this + ".inputStream()"; + } + }; + } + + @Override public void close() throws IOException { + if (closed) return; + closed = true; + source.close(); + buffer.clear(); + } + + @Override public Timeout timeout() { + return source.timeout(); + } + + @Override public String toString() { + return "buffer(" + source + ")"; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java new file mode 100644 index 000000000..501343a2d --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +/** + * A segment of a buffer. + * + *

Each segment in a buffer is a circularly-linked list node referencing + * the following and preceding segments in the buffer. + * + *

Each segment in the pool is a singly-linked list node referencing the rest + * of segments in the pool. + */ +public final class Segment { + /** The size of all segments in bytes. */ + // TODO: Using fixed-size segments makes pooling easier. But it harms memory + // efficiency and encourages copying. Try variable sized segments? + // TODO: Is 2 KiB a good default segment size? + static final int SIZE = 2048; + + final byte[] data = new byte[SIZE]; + + /** The next byte of application data byte to read in this segment. */ + int pos; + + /** The first byte of available data ready to be written to. */ + int limit; + + /** Next segment in a linked or circularly-linked list. */ + Segment next; + + /** Previous segment in a circularly-linked list. */ + Segment prev; + + /** + * Removes this segment of a circularly-linked list and returns its successor. + * Returns null if the list is now empty. + */ + public Segment pop() { + Segment result = next != this ? next : null; + prev.next = next; + next.prev = prev; + next = null; + prev = null; + return result; + } + + /** + * Appends {@code segment} after this segment in the circularly-linked list. + * Returns the pushed segment. + */ + public Segment push(Segment segment) { + segment.prev = this; + segment.next = next; + next.prev = segment; + next = segment; + return segment; + } + + /** + * Splits this head of a circularly-linked list into two segments. The first + * segment contains the data in {@code [pos..pos+byteCount)}. The second + * segment contains the data in {@code [pos+byteCount..limit)}. This can be + * useful when moving partial segments from one buffer to another. + * + *

Returns the new head of the circularly-linked list. + */ + public Segment split(int byteCount) { + int aSize = byteCount; + int bSize = (limit - pos) - byteCount; + if (aSize <= 0 || bSize <= 0) throw new IllegalArgumentException(); + + // Which side of the split is larger? We want to copy as few bytes as possible. + if (aSize < bSize) { + // Create a segment of size 'aSize' before this segment. + Segment before = SegmentPool.getInstance().take(); + System.arraycopy(data, pos, before.data, before.pos, aSize); + pos += aSize; + before.limit += aSize; + prev.push(before); + return before; + } else { + // Create a new segment of size 'bSize' after this segment. + Segment after = SegmentPool.getInstance().take(); + System.arraycopy(data, pos + aSize, after.data, after.pos, bSize); + limit -= bSize; + after.limit += bSize; + push(after); + return this; + } + } + + /** + * Call this when the tail and its predecessor may both be less than half + * full. This will copy data so that segments can be recycled. + */ + public void compact() { + if (prev == this) throw new IllegalStateException(); + if ((prev.limit - prev.pos) + (limit - pos) > SIZE) return; // Cannot compact. + writeTo(prev, limit - pos); + pop(); + SegmentPool.getInstance().recycle(this); + } + + /** Moves {@code byteCount} bytes from this segment to {@code sink}. */ + // TODO: if sink has fewer bytes than this, it may be cheaper to reverse the + // direction of the copy and swap the segments! + public void writeTo(Segment sink, int byteCount) { + if (byteCount + (sink.limit - sink.pos) > SIZE) throw new IllegalArgumentException(); + + if (sink.limit + byteCount > SIZE) { + // We can't fit byteCount bytes at the sink's current position. Compact sink first. + System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos); + sink.limit -= sink.pos; + sink.pos = 0; + } + + System.arraycopy(data, pos, sink.data, sink.limit, byteCount); + sink.limit += byteCount; + pos += byteCount; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java new file mode 100644 index 000000000..f410c8c23 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +/** + * A collection of unused segments, necessary to avoid GC churn and zero-fill. + * This pool is a thread-safe static singleton. + */ +public final class SegmentPool { + private static final SegmentPool INSTANCE = new SegmentPool(); + public static SegmentPool getInstance() { + return INSTANCE; + } + + /** The maximum number of bytes to pool. */ + // TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments? + static final long MAX_SIZE = 64 * 1024; // 64 KiB. + + /** Singly-linked list of segments. */ + private Segment next; + + /** Total bytes in this pool. */ + long byteCount; + + private SegmentPool() { + } + + Segment take() { + synchronized (this) { + if (next != null) { + Segment result = next; + next = result.next; + result.next = null; + byteCount -= Segment.SIZE; + return result; + } + } + return new Segment(); // Pool is empty. Don't zero-fill while holding a lock. + } + + void recycle(Segment segment) { + if (segment.next != null || segment.prev != null) throw new IllegalArgumentException(); + synchronized (this) { + if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full. + byteCount += Segment.SIZE; + segment.next = next; + segment.pos = segment.limit = 0; + next = segment; + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Sink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Sink.java new file mode 100644 index 000000000..d1e3cc626 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Sink.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Receives a stream of bytes. Use this interface to write data wherever it's + * needed: to the network, storage, or a buffer in memory. Sinks may be layered + * to transform received data, such as to compress, encrypt, throttle, or add + * protocol framing. + * + *

Most application code shouldn't operate on a sink directly, but rather + * {@link BufferedSink} which is both more efficient and more convenient. Use + * {@link Okio#buffer(com.koushikdutta.async.http.spdy.okio.Sink)} to wrap any sink with a buffer. + * + *

Sinks are easy to test: just use an {@link Buffer} in your tests, and + * read from it to confirm it received the data that was expected. + * + *

Comparison with OutputStream

+ * This interface is functionally equivalent to {@link java.io.OutputStream}. + * + *

{@code OutputStream} requires multiple layers when emitted data is + * heterogeneous: a {@code DataOutputStream} for primitive values, a {@code + * BufferedOutputStream} for buffering, and {@code OutputStreamWriter} for + * charset encoding. This class uses {@code BufferedSink} for all of the above. + * + *

Sink is also easier to layer: there is no {@linkplain + * java.io.OutputStream#write(int) single-byte write} method that is awkward to + * implement efficiently. + * + *

Interop with OutputStream

+ * Use {@link Okio#sink} to adapt an {@code OutputStream} to a sink. Use {@link + * BufferedSink#outputStream} to adapt a sink to an {@code OutputStream}. + */ +public interface Sink extends Closeable { + /** Removes {@code byteCount} bytes from {@code source} and appends them to this. */ + void write(Buffer source, long byteCount) throws IOException; + + /** Pushes all buffered bytes to their final destination. */ + void flush() throws IOException; + + /** Returns the timeout for this sink. */ + Timeout timeout(); + + /** + * Pushes all buffered bytes to their final destination and releases the + * resources held by this sink. It is an error to write a closed sink. It is + * safe to close a sink more than once. + */ + @Override void close() throws IOException; +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Source.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Source.java new file mode 100644 index 000000000..4f4132252 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Source.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Supplies a stream of bytes. Use this interface to read data from wherever + * it's located: from the network, storage, or a buffer in memory. Sources may + * be layered to transform supplied data, such as to decompress, decrypt, or + * remove protocol framing. + * + *

Most applications shouldn't operate on a source directly, but rather + * {@link BufferedSource} which is both more efficient and more convenient. Use + * {@link Okio#buffer(com.koushikdutta.async.http.spdy.okio.Source)} to wrap any source with a buffer. + * + *

Sources are easy to test: just use an {@link Buffer} in your tests, and + * fill it with the data your application is to read. + * + *

Comparison with InputStream

+ * This interface is functionally equivalent to {@link java.io.InputStream}. + * + *

{@code InputStream} requires multiple layers when consumed data is + * heterogeneous: a {@code DataInputStream} for primitive values, a {@code + * BufferedInputStream} for buffering, and {@code InputStreamReader} for + * strings. This class uses {@code BufferedSource} for all of the above. + * + *

Source avoids the impossible-to-implement {@linkplain + * java.io.InputStream#available available()} method. Instead callers specify + * how many bytes they {@link BufferedSource#require require}. + * + *

Source omits the unsafe-to-compose {@linkplain java.io.InputStream#mark + * mark and reset} state that's tracked by {@code InputStream}; callers instead + * just buffer what they need. + * + *

When implementing a source, you need not worry about the {@linkplain + * java.io.InputStream#read single-byte read} method that is awkward to + * implement efficiently and that returns one of 257 possible values. + * + *

And source has a stronger {@code skip} method: {@link BufferedSource#skip} + * won't return prematurely. + * + *

Interop with InputStream

+ * Use {@link Okio#source} to adapt an {@code InputStream} to a source. Use + * {@link BufferedSource#inputStream} to adapt a source to an {@code + * InputStream}. + */ +public interface Source extends Closeable { + /** + * Removes at least 1, and up to {@code byteCount} bytes from this and appends + * them to {@code sink}. Returns the number of bytes read, or -1 if this + * source is exhausted. + */ + long read(Buffer sink, long byteCount) throws IOException; + + /** Returns the timeout for this source. */ + Timeout timeout(); + + /** + * Closes this source and releases the resources held by this source. It is an + * error to read a closed source. It is safe to close a source more than once. + */ + @Override void close() throws IOException; +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Timeout.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Timeout.java new file mode 100644 index 000000000..3a307e8d7 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Timeout.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.concurrent.TimeUnit; + +/** + * A policy on how much time to spend on a task before giving up. When a task + * times out, it is left in an unspecified state and should be abandoned. For + * example, if reading from a source times out, that source should be closed and + * the read should be retried later. If writing to a sink times out, the same + * rules apply: close the sink and retry later. + * + *

Timeouts and Deadlines

+ * This class offers two complementary controls to define a timeout policy. + * + *

Timeouts specify the maximum time to wait for a single + * operation to complete. Timeouts are typically used to detect problems like + * network partitions. For example, if a remote peer doesn't return any + * data for ten seconds, we may assume that the peer is unavailable. + * + *

Deadlines specify the maximum time to spend on a job, + * composed of one or more operations. Use deadlines to set an upper bound on + * the time invested on a job. For example, a battery-conscious app may limit + * how much time it spends preloading content. + */ +public class Timeout { + /** + * An empty timeout that neither tracks nor detects timeouts. Use this when + * timeouts aren't necessary, such as in implementations whose operations + * do not block. + */ + public static final Timeout NONE = new Timeout() { + @Override public Timeout timeout(long timeout, TimeUnit unit) { + return this; + } + + @Override public Timeout deadlineNanoTime(long deadlineNanoTime) { + return this; + } + + @Override public void throwIfReached() throws IOException { + } + }; + + /** + * True if {@code deadlineNanoTime} is defined. There is no equivalent to null + * or 0 for {@link System#nanoTime}. + */ + private boolean hasDeadline; + private long deadlineNanoTime; + private long timeoutNanos; + + public Timeout() { + } + + /** + * Wait at most {@code timeout} time before aborting an operation. Using a + * per-operation timeout means that as long as forward progress is being made, + * no sequence of operations will fail. + * + *

If {@code timeout == 0}, operations will run indefinitely. (Operating + * system timeouts may still apply.) + */ + public Timeout timeout(long timeout, TimeUnit unit) { + if (timeout < 0) throw new IllegalArgumentException("timeout < 0: " + timeout); + if (unit == null) throw new IllegalArgumentException("unit == null"); + this.timeoutNanos = unit.toNanos(timeout); + return this; + } + + /** Returns the timeout in nanoseconds, or {@code 0} for no timeout. */ + public long timeoutNanos() { + return timeoutNanos; + } + + /** Returns true if a deadline is enabled. */ + public boolean hasDeadline() { + return hasDeadline; + } + + /** + * Returns the {@linkplain System#nanoTime() nano time} when the deadline will + * be reached. + * + * @throws IllegalStateException if no deadline is set. + */ + public long deadlineNanoTime() { + if (!hasDeadline) throw new IllegalStateException("No deadline"); + return deadlineNanoTime; + } + + /** + * Sets the {@linkplain System#nanoTime() nano time} when the deadline will be + * reached. All operations must complete before this time. Use a deadline to + * set a maximum bound on the time spent on a sequence of operations. + */ + public Timeout deadlineNanoTime(long deadlineNanoTime) { + this.hasDeadline = true; + this.deadlineNanoTime = deadlineNanoTime; + return this; + } + + /** Set a deadline of now plus {@code duration} time. */ + public final Timeout deadline(long duration, TimeUnit unit) { + if (duration <= 0) throw new IllegalArgumentException("duration <= 0: " + duration); + if (unit == null) throw new IllegalArgumentException("unit == null"); + return deadlineNanoTime(System.nanoTime() + unit.toNanos(duration)); + } + + /** Clears the timeout. Operating system timeouts may still apply. */ + public Timeout clearTimeout() { + this.timeoutNanos = 0; + return this; + } + + /** Clears the deadline. */ + public Timeout clearDeadline() { + this.hasDeadline = false; + return this; + } + + /** + * Throws an {@link java.io.IOException} if the deadline has been reached or if the + * current thread has been interrupted. This method doesn't detect timeouts; + * that should be implemented to asynchronously abort an in-progress + * operation. + */ + public void throwIfReached() throws IOException { + if (Thread.interrupted()) { + throw new InterruptedIOException(); + } + + if (hasDeadline && System.nanoTime() > deadlineNanoTime) { + throw new IOException("deadline reached"); + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Util.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Util.java new file mode 100644 index 000000000..14775e85a --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Util.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; + +import java.nio.charset.Charset; + +final class Util { + /** A cheap and type-safe constant for the UTF-8 Charset. */ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + private Util() { + } + + public static void checkOffsetAndCount(long size, long offset, long byteCount) { + if ((offset | byteCount) < 0 || offset > size || size - offset < byteCount) { + throw new ArrayIndexOutOfBoundsException( + String.format("size=%s offset=%s byteCount=%s", size, offset, byteCount)); + } + } + + public static short reverseBytesShort(short s) { + int i = s & 0xffff; + int reversed = (i & 0xff00) >>> 8 + | (i & 0x00ff) << 8; + return (short) reversed; + } + + public static int reverseBytesInt(int i) { + return (i & 0xff000000) >>> 24 + | (i & 0x00ff0000) >>> 8 + | (i & 0x0000ff00) << 8 + | (i & 0x000000ff) << 24; + } + + public static long reverseBytesLong(long v) { + return (v & 0xff00000000000000L) >>> 56 + | (v & 0x00ff000000000000L) >>> 40 + | (v & 0x0000ff0000000000L) >>> 24 + | (v & 0x000000ff00000000L) >>> 8 + | (v & 0x00000000ff000000L) << 8 + | (v & 0x0000000000ff0000L) << 24 + | (v & 0x000000000000ff00L) << 40 + | (v & 0x00000000000000ffL) << 56; + } + + /** + * Throws {@code t}, even if the declared throws clause doesn't permit it. + * This is a terrible – but terribly convenient – hack that makes it easy to + * catch and rethrow exceptions after cleanup. See Java Puzzlers #43. + */ + public static void sneakyRethrow(Throwable t) { + Util.sneakyThrow2(t); + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow2(Throwable t) throws T { + throw (T) t; + } +} From 7ce289952764e8a8a7a85c4f43658e06b369ac98 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 24 Jul 2014 19:42:25 -0700 Subject: [PATCH 045/399] decouple the http transport from the client. prep for spdy. --- .../async/http/AsyncHttpClient.java | 58 +++++++------ .../async/http/AsyncHttpClientMiddleware.java | 18 +++- .../async/http/AsyncHttpResponse.java | 3 - .../async/http/AsyncHttpResponseImpl.java | 85 +++++++++---------- .../async/http/AsyncSocketMiddleware.java | 2 +- .../async/http/HttpTransportMiddleware.java | 81 ++++++++++++++++++ .../com/koushikdutta/async/http/Protocol.java | 2 + .../http/cache/ResponseCacheMiddleware.java | 10 +-- 8 files changed, 177 insertions(+), 82 deletions(-) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index 313a9fa10..fb13811dc 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -66,11 +66,13 @@ public void insertMiddleware(AsyncHttpClientMiddleware middleware) { AsyncSSLSocketMiddleware sslSocketMiddleware; AsyncSocketMiddleware socketMiddleware; + HttpTransportMiddleware httpTransportMiddleware; AsyncServer mServer; public AsyncHttpClient(AsyncServer server) { mServer = server; insertMiddleware(socketMiddleware = new AsyncSocketMiddleware(this)); insertMiddleware(sslSocketMiddleware = new AsyncSSLSocketMiddleware(this)); + insertMiddleware(httpTransportMiddleware = new HttpTransportMiddleware()); } @@ -274,7 +276,7 @@ protected void onRequestCompleted(Exception ex) { if (cancel.isCancelled()) return; // 5) after request is sent, set a header timeout - if (cancel.timeoutRunnable != null && data.headers == null) { + if (cancel.timeoutRunnable != null && mHeaders == null) { mServer.removeAllCallbacks(cancel.scheduled); cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request)); } @@ -282,14 +284,12 @@ protected void onRequestCompleted(Exception ex) { @Override public void setDataEmitter(DataEmitter emitter) { - data.response = this; data.bodyEmitter = emitter; synchronized (mMiddleware) { for (AsyncHttpClientMiddleware middleware: mMiddleware) { middleware.onBodyDecoder(data); } } - mHeaders = data.headers; super.setDataEmitter(data.bodyEmitter); @@ -333,31 +333,25 @@ public void setDataEmitter(DataEmitter emitter) { } protected void onHeadersReceived() { - try { - if (cancel.isCancelled()) - return; + super.onHeadersReceived(); + if (cancel.isCancelled()) + return; - // 7) on headers, cancel timeout - if (cancel.timeoutRunnable != null) - mServer.removeAllCallbacks(cancel.scheduled); + // 7) on headers, cancel timeout + if (cancel.timeoutRunnable != null) + mServer.removeAllCallbacks(cancel.scheduled); - // allow the middleware to massage the headers before the body is decoded - request.logv("Received headers:\n" + toString()); + // allow the middleware to massage the headers before the body is decoded + request.logv("Received headers:\n" + toString()); - data.headers = mHeaders; - synchronized (mMiddleware) { - for (AsyncHttpClientMiddleware middleware: mMiddleware) { - middleware.onHeadersReceived(data); - } + synchronized (mMiddleware) { + for (AsyncHttpClientMiddleware middleware: mMiddleware) { + middleware.onHeadersReceived(data); } - mHeaders = data.headers; - - // drop through, and setDataEmitter will be called for the body decoder. - // headers will be further massaged in there. - } - catch (Exception ex) { - reportConnectedCompleted(cancel, ex, null, request, callback); } + + // drop through, and setDataEmitter will be called for the body decoder. + // headers will be further massaged in there. } @Override @@ -390,7 +384,6 @@ protected void report(Exception ex) { } } - @Override public AsyncSocket detachSocket() { request.logd("Detaching socket"); @@ -406,7 +399,24 @@ public AsyncSocket detachSocket() { } }; + data.sendHeadersCallback = new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + if (ex != null) + ret.report(ex); + else + ret.onHeadersSent(); + } + }; + data.response = ret; ret.setSocket(socket); + + synchronized (mMiddleware) { + for (AsyncHttpClientMiddleware middleware: mMiddleware) { + if (middleware.sendHeaders(data)) + break; + } + } } }; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java index c706e536f..b19819914 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java @@ -2,6 +2,7 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.future.Cancellable; @@ -12,6 +13,19 @@ * inspect, manipulate, and handle http requests. */ public interface AsyncHttpClientMiddleware { + public interface ResponseHead { + public String protocol(); + public String message(); + public int code(); + public ResponseHead protocol(String protocol); + public ResponseHead message(String message); + public ResponseHead code(int code); + public Headers headers(); + public ResponseHead headers(Headers headers); + public DataSink sink(); + public ResponseHead sink(DataSink sink); + } + public static class OnRequestData { public UntypedHashtable state = new UntypedHashtable(); public AsyncHttpRequest request; @@ -25,15 +39,15 @@ public static class GetSocketData extends OnRequestData { public static class SendHeaderData extends GetSocketData { public AsyncSocket socket; + public ResponseHead response; public CompletedCallback sendHeadersCallback; } public static class OnHeadersReceivedData extends SendHeaderData { - public Headers headers; +// public Headers headers; } public static class OnBodyData extends OnHeadersReceivedData { - public AsyncHttpResponse response; public DataEmitter bodyEmitter; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java index e7713952c..fa3b71005 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java @@ -8,9 +8,6 @@ public interface AsyncHttpResponse extends DataEmitter { public String protocol(); public String message(); public int code(); - public AsyncHttpResponse protocol(String protocol); - public AsyncHttpResponse message(String message); - public AsyncHttpResponse code(int code); public Headers headers(); public AsyncSocket detachSocket(); public AsyncHttpRequest getRequest(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index fca047354..fa2bb11f5 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -18,7 +18,7 @@ import java.io.IOException; import java.nio.charset.Charset; -abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements AsyncSocket, AsyncHttpResponse { +abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements AsyncSocket, AsyncHttpResponse, AsyncHttpClientMiddleware.ResponseHead { private AsyncHttpRequestBody mWriter; public AsyncSocket getSocket() { @@ -32,59 +32,31 @@ public AsyncHttpRequest getRequest() { void setSocket(AsyncSocket exchange) { mSocket = exchange; - if (mSocket == null) return; - mWriter = mRequest.getBody(); - if (mWriter != null) { - if (mRequest.getHeaders().get("Content-Type") == null) - mRequest.getHeaders().set("Content-Type", mWriter.getContentType()); - if (mWriter.length() >= 0) { - mRequest.getHeaders().set("Content-Length", String.valueOf(mWriter.length())); - mSink = mSocket; - } - else { - mRequest.getHeaders().set("Transfer-Encoding", "Chunked"); - mSink = new ChunkedOutputFilter(mSocket); - } - } - else { - mSink = mSocket; - } - mSocket.setEndCallback(mReporter); - mSocket.setClosedCallback(new CompletedCallback() { - @Override - public void onCompleted(Exception ex) { - // TODO: do we care? throw if socket is still writing or something? - } - }); - String rl = mRequest.getRequestLine().toString(); - String rs = mRequest.getHeaders().toPrefixString(rl); - mRequest.logv("\n" + rs); - Util.writeAll(exchange, rs.getBytes(), new CompletedCallback() { - @Override - public void onCompleted(Exception ex) { - if (mWriter != null) { - mWriter.write(mRequest, AsyncHttpResponseImpl.this, new CompletedCallback() { - @Override - public void onCompleted(Exception ex) { - onRequestCompleted(ex); - } - }); - } else { - onRequestCompleted(null); - } - } - }); + mWriter = mRequest.getBody(); LineEmitter liner = new LineEmitter(); exchange.setDataCallback(liner); liner.setLineCallback(mHeaderCallback); } + protected void onHeadersSent() { + if (mWriter != null) { + mWriter.write(mRequest, AsyncHttpResponseImpl.this, new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + onRequestCompleted(ex); + } + }); + } else { + onRequestCompleted(null); + } + } + protected void onRequestCompleted(Exception ex) { } @@ -100,7 +72,8 @@ public void onCompleted(Exception error) { } }; - protected abstract void onHeadersReceived(); + protected void onHeadersReceived() { + } StringCallback mHeaderCallback = new StringCallback() { private Headers mRawHeaders = new Headers(); @@ -179,6 +152,12 @@ public Headers headers() { return mHeaders; } + @Override + public AsyncHttpClientMiddleware.ResponseHead headers(Headers headers) { + mHeaders = headers; + return this; + } + int code; @Override public int code() { @@ -186,19 +165,19 @@ public int code() { } @Override - public AsyncHttpResponse code(int code) { + public AsyncHttpClientMiddleware.ResponseHead code(int code) { this.code = code; return this; } @Override - public AsyncHttpResponse protocol(String protocol) { + public AsyncHttpClientMiddleware.ResponseHead protocol(String protocol) { this.protocol = protocol; return this; } @Override - public AsyncHttpResponse message(String message) { + public AsyncHttpClientMiddleware.ResponseHead message(String message) { this.message = message; return this; } @@ -232,6 +211,18 @@ private void assertContent() { } DataSink mSink; + + @Override + public DataSink sink() { + return mSink; + } + + @Override + public AsyncHttpClientMiddleware.ResponseHead sink(DataSink sink) { + mSink = sink; + return this; + } + @Override public void write(ByteBufferList bb) { assertContent(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java index 37ffe5cc6..5c11684de 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java @@ -358,7 +358,7 @@ public void onRequestComplete(final OnRequestCompleteData data) { data.socket.close(); return; } - if (!HttpUtil.isKeepAlive(data.response.protocol(), data.headers) + if (!HttpUtil.isKeepAlive(data.response.protocol(), data.response.headers()) || !HttpUtil.isKeepAlive(Protocol.HTTP_1_1, data.request.getHeaders())) { data.request.logv("closing out socket (not keep alive)"); data.socket.close(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java new file mode 100644 index 000000000..1ca6b5ae0 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java @@ -0,0 +1,81 @@ +package com.koushikdutta.async.http; + +import com.koushikdutta.async.Util; +import com.koushikdutta.async.callback.CompletedCallback; +import com.koushikdutta.async.http.body.AsyncHttpRequestBody; +import com.koushikdutta.async.http.filter.ChunkedOutputFilter; + +/** + * Created by koush on 7/24/14. + */ +public class HttpTransportMiddleware extends SimpleMiddleware { + + @Override + public boolean sendHeaders(final SendHeaderData data) { + Protocol p = Protocol.get(data.protocol); + if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1) + return super.sendHeaders(data); + + AsyncHttpRequest request = data.request; + AsyncHttpRequestBody requestBody = data.request.getBody(); + + if (requestBody != null) { + if (request.getHeaders().get("Content-Type") == null) + request.getHeaders().set("Content-Type", requestBody.getContentType()); + if (requestBody.length() >= 0) { + request.getHeaders().set("Content-Length", String.valueOf(requestBody.length())); + data.response.sink(data.socket); + } else { + request.getHeaders().set("Transfer-Encoding", "Chunked"); + data.response.sink(new ChunkedOutputFilter(data.socket)); + } + } + + String rl = request.getRequestLine().toString(); + String rs = request.getHeaders().toPrefixString(rl); + request.logv("\n" + rs); + + Util.writeAll(data.socket, rs.getBytes(), new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + data.sendHeadersCallback.onCompleted(ex); + } + }); + +// LineEmitter.StringCallback headerCallback = new LineEmitter.StringCallback() { +// Headers mRawHeaders = new Headers(); +// String statusLine; +// +// @Override +// public void onStringAvailable(String s) { +// try { +// if (statusLine == null) { +// statusLine = s; +// } +// else if (!"\r".equals(s)) { +// mRawHeaders.addLine(s); +// } +// else { +// String[] parts = statusLine.split(" ", 3); +// if (parts.length != 3) +// throw new Exception(new IOException("Not HTTP")); +// +// data.response.headers(mRawHeaders); +// data.response.protocol(parts[0]); +// data.response.code(Integer.parseInt(parts[1])); +// data.response.message(parts[2]); +// data.sendHeadersCallback.onCompleted(null); +// } +// } +// catch (Exception ex) { +// data.sendHeadersCallback.onCompleted(ex); +// } +// } +// }; +// +// LineEmitter liner = new LineEmitter(); +// data.socket.setDataCallback(liner); +// liner.setLineCallback(headerCallback); + return true; + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Protocol.java b/AndroidAsync/src/com/koushikdutta/async/http/Protocol.java index 8e5a46c46..46134280c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/Protocol.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/Protocol.java @@ -75,6 +75,8 @@ public enum Protocol { * Returns the protocol identified by {@code protocol}. */ public static Protocol get(String protocol) { + if (protocol == null) + return null; return protocols.get(protocol.toLowerCase()); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java index 447c81b05..57e389574 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java @@ -216,25 +216,25 @@ public int getCacheStoreCount() { public void onBodyDecoder(OnBodyData data) { CachedSocket cached = com.koushikdutta.async.Util.getWrappedSocket(data.socket, CachedSocket.class); if (cached != null) { - data.headers.set(SERVED_FROM, CACHE); + data.response.headers().set(SERVED_FROM, CACHE); return; } CacheData cacheData = data.state.get("cache-data"); - RawHeaders rh = RawHeaders.fromMultimap(data.headers.getMultiMap()); + RawHeaders rh = RawHeaders.fromMultimap(data.response.headers().getMultiMap()); + rh.removeAll("Content-Length"); rh.setStatusLine(String.format("%s %s %s", data.response.protocol(), data.response.code(), data.response.message())); ResponseHeaders networkResponse = new ResponseHeaders(data.request.getUri(), rh); data.state.put("response-headers", networkResponse); if (cacheData != null) { if (cacheData.cachedResponseHeaders.validate(networkResponse)) { data.request.logi("Serving response from conditional cache"); - data.headers.removeAll("Content-Length"); ResponseHeaders combined = cacheData.cachedResponseHeaders.combine(networkResponse); - data.headers = new Headers(combined.getHeaders().toMultimap()); + data.response.headers(new Headers(combined.getHeaders().toMultimap())); data.response.code(combined.getHeaders().getResponseCode()); data.response.message(combined.getHeaders().getResponseMessage()); - data.headers.set(SERVED_FROM, CONDITIONAL_CACHE); + data.response.headers().set(SERVED_FROM, CONDITIONAL_CACHE); conditionalCacheHitCount++; CachedBodyEmitter bodySpewer = new CachedBodyEmitter(cacheData.candidate, cacheData.contentLength); From 893442cba5cc5c20ea0be7953860513ab7e4e325 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 24 Jul 2014 22:52:56 -0700 Subject: [PATCH 046/399] finished up transport refactor... time for some spdy. --- .../async/http/AsyncHttpClient.java | 297 +++++++++--------- .../async/http/AsyncHttpClientMiddleware.java | 14 +- .../async/http/AsyncHttpResponseImpl.java | 68 +--- .../async/http/HttpTransportMiddleware.java | 98 +++--- .../async/http/SimpleMiddleware.java | 2 +- .../async/http/spdy/HttpTransport.java | 15 - 6 files changed, 237 insertions(+), 257 deletions(-) delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/HttpTransport.java diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index fb13811dc..ee4f8054c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -160,7 +160,7 @@ private void reportConnectedCompleted(FutureAsyncHttpResponse cancel, Exception } if (complete) { callback.onConnectCompleted(ex, response); - assert ex != null || response.getSocket() == null || response.getDataCallback() != null || response.isPaused(); + assert ex != null || response.socket() == null || response.getDataCallback() != null || response.isPaused(); return; } @@ -266,174 +266,189 @@ public void onConnectCompleted(Exception ex, AsyncSocket socket) { return; } - // 4) wait for request to be sent fully - // and - // 6) wait for headers - final AsyncHttpResponseImpl ret = new AsyncHttpResponseImpl(request) { - @Override - protected void onRequestCompleted(Exception ex) { - request.logv("request completed"); - if (cancel.isCancelled()) - return; - // 5) after request is sent, set a header timeout - if (cancel.timeoutRunnable != null && mHeaders == null) { - mServer.removeAllCallbacks(cancel.scheduled); - cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request)); - } - } + executeSocket(request, redirectCount, cancel, callback, data); + } + }; - @Override - public void setDataEmitter(DataEmitter emitter) { - data.bodyEmitter = emitter; - synchronized (mMiddleware) { - for (AsyncHttpClientMiddleware middleware: mMiddleware) { - middleware.onBodyDecoder(data); - } - } + // set up the system default proxy and connect + setupAndroidProxy(request); - super.setDataEmitter(data.bodyEmitter); - - Headers headers = mHeaders; - int responseCode = code(); - if ((responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == 307) && request.getFollowRedirect()) { - String location = headers.get("Location"); - Uri redirect; - try { - redirect = Uri.parse(location); - if (redirect.getScheme() == null) { - redirect = Uri.parse(new URL(new URL(uri.toString()), location).toString()); - } - } - catch (Exception e) { - reportConnectedCompleted(cancel, e, this, request, callback); - return; - } - final String method = request.getMethod().equals(AsyncHttpHead.METHOD) ? AsyncHttpHead.METHOD : AsyncHttpGet.METHOD; - AsyncHttpRequest newReq = new AsyncHttpRequest(redirect, method); - newReq.executionTime = request.executionTime; - newReq.logLevel = request.logLevel; - newReq.LOGTAG = request.LOGTAG; - newReq.proxyHost = request.proxyHost; - newReq.proxyPort = request.proxyPort; - setupAndroidProxy(newReq); - copyHeader(request, newReq, "User-Agent"); - copyHeader(request, newReq, "Range"); - request.logi("Redirecting"); - newReq.logi("Redirected"); - execute(newReq, redirectCount + 1, cancel, callback); - - setDataCallback(new NullDataCallback()); - return; - } + synchronized (mMiddleware) { + for (AsyncHttpClientMiddleware middleware: mMiddleware) { + Cancellable socketCancellable = middleware.getSocket(data); + if (socketCancellable != null) { + data.socketCancellable = socketCancellable; + cancel.setParent(socketCancellable); + return; + } + } + } + reportConnectedCompleted(cancel, new IllegalArgumentException("invalid uri"), null, request, callback); + } - request.logv("Final (post cache response) headers:\n" + toString()); + private void executeSocket(final AsyncHttpRequest request, final int redirectCount, + final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback, + final OnRequestCompleteData data) { + // 4) wait for request to be sent fully + // and + // 6) wait for headers + final AsyncHttpResponseImpl ret = new AsyncHttpResponseImpl(request) { + @Override + protected void onRequestCompleted(Exception ex) { + request.logv("request completed"); + if (cancel.isCancelled()) + return; + // 5) after request is sent, set a header timeout + if (cancel.timeoutRunnable != null && mHeaders == null) { + mServer.removeAllCallbacks(cancel.scheduled); + cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request)); + } + } - // at this point the headers are done being modified - reportConnectedCompleted(cancel, null, this, request, callback); + @Override + public void setDataEmitter(DataEmitter emitter) { + data.bodyEmitter = emitter; + synchronized (mMiddleware) { + for (AsyncHttpClientMiddleware middleware: mMiddleware) { + middleware.onBodyDecoder(data); } + } - protected void onHeadersReceived() { - super.onHeadersReceived(); - if (cancel.isCancelled()) - return; + super.setDataEmitter(data.bodyEmitter); - // 7) on headers, cancel timeout - if (cancel.timeoutRunnable != null) - mServer.removeAllCallbacks(cancel.scheduled); + Headers headers = mHeaders; + int responseCode = code(); + if ((responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == 307) && request.getFollowRedirect()) { + String location = headers.get("Location"); + Uri redirect; + try { + redirect = Uri.parse(location); + if (redirect.getScheme() == null) { + redirect = Uri.parse(new URL(new URL(request.getUri().toString()), location).toString()); + } + } + catch (Exception e) { + reportConnectedCompleted(cancel, e, this, request, callback); + return; + } + final String method = request.getMethod().equals(AsyncHttpHead.METHOD) ? AsyncHttpHead.METHOD : AsyncHttpGet.METHOD; + AsyncHttpRequest newReq = new AsyncHttpRequest(redirect, method); + newReq.executionTime = request.executionTime; + newReq.logLevel = request.logLevel; + newReq.LOGTAG = request.LOGTAG; + newReq.proxyHost = request.proxyHost; + newReq.proxyPort = request.proxyPort; + setupAndroidProxy(newReq); + copyHeader(request, newReq, "User-Agent"); + copyHeader(request, newReq, "Range"); + request.logi("Redirecting"); + newReq.logi("Redirected"); + execute(newReq, redirectCount + 1, cancel, callback); + + setDataCallback(new NullDataCallback()); + return; + } - // allow the middleware to massage the headers before the body is decoded - request.logv("Received headers:\n" + toString()); + request.logv("Final (post cache response) headers:\n" + toString()); - synchronized (mMiddleware) { - for (AsyncHttpClientMiddleware middleware: mMiddleware) { - middleware.onHeadersReceived(data); - } - } + // at this point the headers are done being modified + reportConnectedCompleted(cancel, null, this, request, callback); + } - // drop through, and setDataEmitter will be called for the body decoder. - // headers will be further massaged in there. - } + protected void onHeadersReceived() { + super.onHeadersReceived(); + if (cancel.isCancelled()) + return; - @Override - protected void report(Exception ex) { - if (ex != null) - request.loge("exception during response", ex); - if (cancel.isCancelled()) - return; - if (ex instanceof AsyncSSLException) { - request.loge("SSL Exception", ex); - AsyncSSLException ase = (AsyncSSLException)ex; - request.onHandshakeException(ase); - if (ase.getIgnore()) - return; - } - final AsyncSocket socket = getSocket(); - if (socket == null) - return; - super.report(ex); - if (!socket.isOpen() || ex != null) { - if (headers() == null && ex != null) - reportConnectedCompleted(cancel, ex, null, request, callback); - } + // 7) on headers, cancel timeout + if (cancel.timeoutRunnable != null) + mServer.removeAllCallbacks(cancel.scheduled); - data.exception = ex; - synchronized (mMiddleware) { - for (AsyncHttpClientMiddleware middleware: mMiddleware) { - middleware.onRequestComplete(data); - } - } - } + // allow the middleware to massage the headers before the body is decoded + request.logv("Received headers:\n" + toString()); - @Override - public AsyncSocket detachSocket() { - request.logd("Detaching socket"); - AsyncSocket socket = getSocket(); - if (socket == null) - return null; - socket.setWriteableCallback(null); - socket.setClosedCallback(null); - socket.setEndCallback(null); - socket.setDataCallback(null); - setSocket(null); - return socket; + synchronized (mMiddleware) { + for (AsyncHttpClientMiddleware middleware: mMiddleware) { + middleware.onHeadersReceived(data); } - }; + } - data.sendHeadersCallback = new CompletedCallback() { - @Override - public void onCompleted(Exception ex) { - if (ex != null) - ret.report(ex); - else - ret.onHeadersSent(); - } - }; - data.response = ret; - ret.setSocket(socket); + // drop through, and setDataEmitter will be called for the body decoder. + // headers will be further massaged in there. + } + + @Override + protected void report(Exception ex) { + if (ex != null) + request.loge("exception during response", ex); + if (cancel.isCancelled()) + return; + if (ex instanceof AsyncSSLException) { + request.loge("SSL Exception", ex); + AsyncSSLException ase = (AsyncSSLException)ex; + request.onHandshakeException(ase); + if (ase.getIgnore()) + return; + } + final AsyncSocket socket = socket(); + if (socket == null) + return; + super.report(ex); + if (!socket.isOpen() || ex != null) { + if (headers() == null && ex != null) + reportConnectedCompleted(cancel, ex, null, request, callback); + } + data.exception = ex; synchronized (mMiddleware) { for (AsyncHttpClientMiddleware middleware: mMiddleware) { - if (middleware.sendHeaders(data)) - break; + middleware.onRequestComplete(data); } } } + + @Override + public AsyncSocket detachSocket() { + request.logd("Detaching socket"); + AsyncSocket socket = socket(); + if (socket == null) + return null; + socket.setWriteableCallback(null); + socket.setClosedCallback(null); + socket.setEndCallback(null); + socket.setDataCallback(null); + setSocket(null); + return socket; + } }; - // set up the system default proxy and connect - setupAndroidProxy(request); + data.sendHeadersCallback = new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + if (ex != null) + ret.report(ex); + else + ret.onHeadersSent(); + } + }; + data.receiveHeadersCallback = new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + if (ex != null) + ret.report(ex); + else + ret.onHeadersReceived(); + } + }; + data.response = ret; + ret.setSocket(data.socket); synchronized (mMiddleware) { for (AsyncHttpClientMiddleware middleware: mMiddleware) { - Cancellable socketCancellable = middleware.getSocket(data); - if (socketCancellable != null) { - data.socketCancellable = socketCancellable; - cancel.setParent(socketCancellable); - return; - } + if (middleware.exchangeHeaders(data)) + break; } } - reportConnectedCompleted(cancel, new IllegalArgumentException("invalid uri"), null, request, callback); } public static abstract class RequestCallbackBase implements RequestCallback { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java index b19819914..edce25dda 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java @@ -14,6 +14,7 @@ */ public interface AsyncHttpClientMiddleware { public interface ResponseHead { + public AsyncSocket socket(); public String protocol(); public String message(); public int code(); @@ -24,6 +25,8 @@ public interface ResponseHead { public ResponseHead headers(Headers headers); public DataSink sink(); public ResponseHead sink(DataSink sink); + public DataEmitter emitter(); + public ResponseHead emitter(DataEmitter emitter); } public static class OnRequestData { @@ -37,14 +40,14 @@ public static class GetSocketData extends OnRequestData { public String protocol; } - public static class SendHeaderData extends GetSocketData { + public static class ExchangeHeaderData extends GetSocketData { public AsyncSocket socket; public ResponseHead response; public CompletedCallback sendHeadersCallback; + public CompletedCallback receiveHeadersCallback; } - public static class OnHeadersReceivedData extends SendHeaderData { -// public Headers headers; + public static class OnHeadersReceivedData extends ExchangeHeaderData { } public static class OnBodyData extends OnHeadersReceivedData { @@ -69,11 +72,12 @@ public static class OnRequestCompleteData extends OnBodyData { public Cancellable getSocket(GetSocketData data); /** - * Called before the headers are sent via the socket + * Called before when the headers are sent and received via the socket. + * Implementers return true to denote they will manage header exchange. * @param data * @return */ - public boolean sendHeaders(SendHeaderData data); + public boolean exchangeHeaders(ExchangeHeaderData data); /** * Called once the headers have been received via the socket diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index fa2bb11f5..7938b935e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -19,9 +19,7 @@ import java.nio.charset.Charset; abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements AsyncSocket, AsyncHttpResponse, AsyncHttpClientMiddleware.ResponseHead { - private AsyncHttpRequestBody mWriter; - - public AsyncSocket getSocket() { + public AsyncSocket socket() { return mSocket; } @@ -36,17 +34,12 @@ void setSocket(AsyncSocket exchange) { return; mSocket.setEndCallback(mReporter); - - mWriter = mRequest.getBody(); - - LineEmitter liner = new LineEmitter(); - exchange.setDataCallback(liner); - liner.setLineCallback(mHeaderCallback); } protected void onHeadersSent() { - if (mWriter != null) { - mWriter.write(mRequest, AsyncHttpResponseImpl.this, new CompletedCallback() { + AsyncHttpRequestBody requestBody = mRequest.getBody(); + if (requestBody != null) { + requestBody.write(mRequest, AsyncHttpResponseImpl.this, new CompletedCallback() { @Override public void onCompleted(Exception ex) { onRequestCompleted(ex); @@ -75,48 +68,17 @@ public void onCompleted(Exception error) { protected void onHeadersReceived() { } - StringCallback mHeaderCallback = new StringCallback() { - private Headers mRawHeaders = new Headers(); - private String statusLine; - @Override - public void onStringAvailable(String s) { - try { - if (statusLine == null) { - statusLine = s; - } - else if (!"\r".equals(s)) { - mRawHeaders.addLine(s); - } - else { - String[] parts = statusLine.split(" ", 3); - if (parts.length != 3) - throw new Exception(new IOException("Not HTTP")); - - protocol = parts[0]; - code = Integer.parseInt(parts[1]); - message = parts[2]; - mHeaders = mRawHeaders; - onHeadersReceived(); - // socket may get detached after headers (websocket) - if (mSocket == null) - return; - DataEmitter emitter; - // HEAD requests must not return any data. They still may - // return content length, etc, which will confuse the body decoder - if (AsyncHttpHead.METHOD.equalsIgnoreCase(mRequest.getMethod())) { - emitter = HttpUtil.EndEmitter.create(getServer(), null); - } - else { - emitter = HttpUtil.getBodyDecoder(mSocket, Protocol.get(protocol), mHeaders, false); - } - setDataEmitter(emitter); - } - } - catch (Exception ex) { - report(ex); - } - } - }; + + @Override + public DataEmitter emitter() { + return getDataEmitter(); + } + + @Override + public AsyncHttpClientMiddleware.ResponseHead emitter(DataEmitter emitter) { + setDataEmitter(emitter); + return this; + } @Override protected void report(Exception e) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java index 1ca6b5ae0..837ca925f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java @@ -1,20 +1,23 @@ package com.koushikdutta.async.http; +import com.koushikdutta.async.AsyncSocket; +import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.LineEmitter; import com.koushikdutta.async.Util; -import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.http.filter.ChunkedOutputFilter; +import java.io.IOException; + /** * Created by koush on 7/24/14. */ public class HttpTransportMiddleware extends SimpleMiddleware { - @Override - public boolean sendHeaders(final SendHeaderData data) { + public boolean exchangeHeaders(final ExchangeHeaderData data) { Protocol p = Protocol.get(data.protocol); if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1) - return super.sendHeaders(data); + return super.exchangeHeaders(data); AsyncHttpRequest request = data.request; AsyncHttpRequestBody requestBody = data.request.getBody(); @@ -35,47 +38,58 @@ public boolean sendHeaders(final SendHeaderData data) { String rs = request.getHeaders().toPrefixString(rl); request.logv("\n" + rs); - Util.writeAll(data.socket, rs.getBytes(), new CompletedCallback() { + Util.writeAll(data.socket, rs.getBytes(), data.sendHeadersCallback); + + LineEmitter.StringCallback headerCallback = new LineEmitter.StringCallback() { + Headers mRawHeaders = new Headers(); + String statusLine; + @Override - public void onCompleted(Exception ex) { - data.sendHeadersCallback.onCompleted(ex); + public void onStringAvailable(String s) { + try { + if (statusLine == null) { + statusLine = s; + } + else if (!"\r".equals(s)) { + mRawHeaders.addLine(s); + } + else { + String[] parts = statusLine.split(" ", 3); + if (parts.length != 3) + throw new Exception(new IOException("Not HTTP")); + + data.response.headers(mRawHeaders); + String protocol = parts[0]; + data.response.protocol(protocol); + data.response.code(Integer.parseInt(parts[1])); + data.response.message(parts[2]); + data.receiveHeadersCallback.onCompleted(null); + + // socket may get detached after headers (websocket) + AsyncSocket socket = data.response.socket(); + if (socket == null) + return; + DataEmitter emitter; + // HEAD requests must not return any data. They still may + // return content length, etc, which will confuse the body decoder + if (AsyncHttpHead.METHOD.equalsIgnoreCase(data.request.getMethod())) { + emitter = HttpUtil.EndEmitter.create(socket.getServer(), null); + } + else { + emitter = HttpUtil.getBodyDecoder(socket, Protocol.get(protocol), mRawHeaders, false); + } + data.response.emitter(emitter); + } + } + catch (Exception ex) { + data.receiveHeadersCallback.onCompleted(ex); + } } - }); + }; -// LineEmitter.StringCallback headerCallback = new LineEmitter.StringCallback() { -// Headers mRawHeaders = new Headers(); -// String statusLine; -// -// @Override -// public void onStringAvailable(String s) { -// try { -// if (statusLine == null) { -// statusLine = s; -// } -// else if (!"\r".equals(s)) { -// mRawHeaders.addLine(s); -// } -// else { -// String[] parts = statusLine.split(" ", 3); -// if (parts.length != 3) -// throw new Exception(new IOException("Not HTTP")); -// -// data.response.headers(mRawHeaders); -// data.response.protocol(parts[0]); -// data.response.code(Integer.parseInt(parts[1])); -// data.response.message(parts[2]); -// data.sendHeadersCallback.onCompleted(null); -// } -// } -// catch (Exception ex) { -// data.sendHeadersCallback.onCompleted(ex); -// } -// } -// }; -// -// LineEmitter liner = new LineEmitter(); -// data.socket.setDataCallback(liner); -// liner.setLineCallback(headerCallback); + LineEmitter liner = new LineEmitter(); + data.socket.setDataCallback(liner); + liner.setLineCallback(headerCallback); return true; } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java index 7fe545d86..242fdabbc 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java @@ -25,7 +25,7 @@ public void onRequestComplete(OnRequestCompleteData data) { } @Override - public boolean sendHeaders(SendHeaderData data) { + public boolean exchangeHeaders(ExchangeHeaderData data) { return false; } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/HttpTransport.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/HttpTransport.java deleted file mode 100644 index 0d017bba6..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/HttpTransport.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.koushikdutta.async.http.spdy; - -import com.koushikdutta.async.http.AsyncHttpClientMiddleware; -import com.koushikdutta.async.http.SimpleMiddleware; - -/** - * Created by koush on 7/19/14. - */ -public class HttpTransport extends SimpleMiddleware { - @Override - public boolean sendHeaders(SendHeaderData data) { - return super.sendHeaders(data); - } - -} From f9ac08876bac04052cce8b78fd05e5cf5fe2fe0e Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sat, 26 Jul 2014 19:27:30 -0700 Subject: [PATCH 047/399] spdy is working --- .../src/com/koushikdutta/async/Util.java | 22 ++ .../async/http/AsyncSSLSocketMiddleware.java | 12 +- .../async/http/spdy/AsyncSpdyConnection.java | 260 ++++++++++++++---- .../async/http/spdy/ByteBufferListSink.java | 52 ++++ .../async/http/spdy/ByteBufferListSource.java | 170 ++---------- .../async/http/spdy/SpdyMiddleware.java | 137 ++++++++- .../async/http/spdy/SpdyTransport.java | 61 ++++ .../okhttp/internal/spdy/FrameReader.java | 2 +- .../okhttp/internal/spdy/Http20Draft13.java | 8 +- .../spdy/okhttp/internal/spdy/Settings.java | 4 +- .../http/spdy/okhttp/internal/spdy/Spdy3.java | 12 +- .../async/http/spdy/okio/Buffer.java | 4 +- .../async/http/spdy/okio/InflaterSource.java | 160 +++++------ .../async/http/spdy/okio/Segment.java | 6 +- .../async/http/spdy/okio/SegmentPool.java | 2 +- 15 files changed, 607 insertions(+), 305 deletions(-) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSink.java create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java diff --git a/AndroidAsync/src/com/koushikdutta/async/Util.java b/AndroidAsync/src/com/koushikdutta/async/Util.java index 5dc0359bc..5e2666224 100644 --- a/AndroidAsync/src/com/koushikdutta/async/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/Util.java @@ -222,4 +222,26 @@ public static DataEmitter getWrappedDataEmitter(DataEmitter emitter, Class wrapp } return null; } + + public static void end(DataEmitter emitter, Exception e) { + if (emitter == null) + return; + end(emitter.getEndCallback(), e); + } + + public static void end(CompletedCallback end, Exception e) { + if (end != null) + end.onCompleted(e); + } + + public static void writable(DataSink emitter) { + if (emitter == null) + return; + writable(emitter.getWriteableCallback()); + } + + public static void writable(WritableCallback writable) { + if (writable != null) + writable.onWriteable(); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java index 59fa0f7c6..01f3fbeec 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSSLSocketMiddleware.java @@ -68,7 +68,7 @@ protected SSLEngine createConfiguredSSLEngine(String host, int port) { return sslEngine; } - protected AsyncSSLSocketWrapper.HandshakeCallback createHandshakeCallback(final ConnectCallback callback) { + protected AsyncSSLSocketWrapper.HandshakeCallback createHandshakeCallback(GetSocketData data, final ConnectCallback callback) { return new AsyncSSLSocketWrapper.HandshakeCallback() { @Override public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { @@ -77,15 +77,15 @@ public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { }; } - protected void tryHandshake(final ConnectCallback callback, AsyncSocket socket, final Uri uri, final int port) { + protected void tryHandshake(AsyncSocket socket, GetSocketData data, final Uri uri, final int port, final ConnectCallback callback) { AsyncSSLSocketWrapper.handshake(socket, uri.getHost(), port, createConfiguredSSLEngine(uri.getHost(), port), trustManagers, hostnameVerifier, true, - createHandshakeCallback(callback)); + createHandshakeCallback(data, callback)); } @Override - protected ConnectCallback wrapCallback(GetSocketData data, final Uri uri, final int port, final boolean proxied, final ConnectCallback callback) { + protected ConnectCallback wrapCallback(final GetSocketData data, final Uri uri, final int port, final boolean proxied, final ConnectCallback callback) { return new ConnectCallback() { @Override public void onConnectCompleted(Exception ex, final AsyncSocket socket) { @@ -95,7 +95,7 @@ public void onConnectCompleted(Exception ex, final AsyncSocket socket) { } if (!proxied) { - tryHandshake(callback, socket, uri, port); + tryHandshake(socket, data, uri, port, callback); return; } @@ -127,7 +127,7 @@ public void onStringAvailable(String s) { socket.setDataCallback(null); socket.setEndCallback(null); if (TextUtils.isEmpty(s.trim())) { - tryHandshake(callback, socket, uri, port); + tryHandshake(socket, data, uri, port, callback); } else { callback.onConnectCompleted(new IOException("unknown second status line"), socket); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index 18e856cf1..066f92a18 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -3,12 +3,15 @@ import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.BufferedDataEmitter; +import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; +import com.koushikdutta.async.future.SimpleFuture; +import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.ErrorCode; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameReader; @@ -20,8 +23,12 @@ import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Variant; +import com.koushikdutta.async.http.spdy.okio.BufferedSink; import com.koushikdutta.async.http.spdy.okio.BufferedSource; import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okio.Okio; + +import junit.framework.Assert; import java.io.IOException; import java.util.Hashtable; @@ -37,31 +44,126 @@ public class AsyncSpdyConnection implements FrameReader.Handler { BufferedDataEmitter emitter; AsyncSocket socket; + BufferedDataSink bufferedSocket; FrameReader reader; FrameWriter writer; Variant variant; - SpdySocket zero = new SpdySocket(0, false, false, null); +// SpdySocket zero = new SpdySocket(0, false, false, null); ByteBufferListSource source = new ByteBufferListSource(); + ByteBufferListSink sink = new ByteBufferListSink() { + @Override + public void flush() throws IOException { + AsyncSpdyConnection.this.flush(); + } + }; + BufferedSource bufferedSource; + BufferedSink bufferedSink; Hashtable sockets = new Hashtable(); Protocol protocol; boolean client = true; - private class SpdySocket implements AsyncSocket { + public void flush() { + bufferedSocket.write(sink); + } + + /** + * Returns a new locally-initiated stream. + * + * @param out true to create an output stream that we can use to send data to the remote peer. + * Corresponds to {@code FLAG_FIN}. + * @param in true to create an input stream that the remote peer can use to send data to us. + * Corresponds to {@code FLAG_UNIDIRECTIONAL}. + */ + public SpdySocket newStream(List

requestHeaders, boolean out, boolean in) throws IOException { + return newStream(0, requestHeaders, out, in); + } + + private SpdySocket newStream(int associatedStreamId, List
requestHeaders, boolean out, + boolean in) throws IOException { + boolean outFinished = !out; + boolean inFinished = !in; + SpdySocket socket; + int streamId; + + if (shutdown) { + throw new IOException("shutdown"); + } + + streamId = nextStreamId; + nextStreamId += 2; + socket = new SpdySocket(streamId, outFinished, inFinished, requestHeaders); + if (socket.isOpen()) { + sockets.put(streamId, socket); +// setIdle(false); + } + if (associatedStreamId == 0) { + writer.synStream(outFinished, inFinished, streamId, associatedStreamId, + requestHeaders); + } else if (client) { + throw new IllegalArgumentException("client streams shouldn't have associated stream IDs"); + } else { // HTTP/2 has a PUSH_PROMISE frame. + writer.pushPromise(associatedStreamId, streamId, requestHeaders); + } + + if (!out) { + writer.flush(); + } + + return socket; + } + + int totalWindowRead; + void updateWindowRead(int length) { + totalWindowRead += length; + if (totalWindowRead >= okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) { + try { + writer.windowUpdate(0, totalWindowRead); + } + catch (IOException e) { + throw new AssertionError(e); + } + totalWindowRead = 0; + } + } + + public class SpdySocket implements AsyncSocket { long bytesLeftInWriteWindow; WritableCallback writable; final int id; CompletedCallback closedCallback; CompletedCallback endCallback; DataCallback dataCallback; - ByteBufferList pending = new ByteBufferList(); + ByteBufferListSink pending = new ByteBufferListSink(); + SimpleFuture> headers = new SimpleFuture>(); + boolean isOpen = true; + int totalWindowRead; - public SpdySocket(int id, boolean outFinished, boolean inFinished, List
headerBlock) { - this.id = id; + public SimpleFuture> headers() { + return headers; } - private void report(Exception e) { - if (endCallback != null) - endCallback.onCompleted(e); + void updateWindowRead(int length) { + totalWindowRead += length; + if (totalWindowRead >= okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) { + try { + writer.windowUpdate(id, totalWindowRead); + } + catch (IOException e) { + throw new AssertionError(e); + } + totalWindowRead = 0; + } + AsyncSpdyConnection.this.updateWindowRead(length); + } + + public SpdySocket(int id, boolean outFinished, boolean inFinished, List
headerBlock) { + this.id = id; + try { + writer.windowUpdate(id, DEFAULT_INITIAL_WINDOW_SIZE); + } + catch (IOException e) { + throw new AssertionError(e); + } } public boolean isLocallyInitiated() { @@ -72,8 +174,8 @@ public boolean isLocallyInitiated() { public void addBytesToWriteWindow(long delta) { long prev = bytesLeftInWriteWindow; bytesLeftInWriteWindow += delta; - if (writable != null && bytesLeftInWriteWindow > 0 && prev <= 0) - writable.onWriteable(); + if (bytesLeftInWriteWindow > 0 && prev <= 0) + Util.writable(writable); } @Override @@ -109,7 +211,7 @@ public void resume() { @Override public void close() { - + isOpen = false; } @Override @@ -134,7 +236,7 @@ public String charset() { @Override public void write(ByteBufferList bb) { - + System.out.println("writing!"); } @Override @@ -149,7 +251,7 @@ public WritableCallback getWriteableCallback() { @Override public boolean isOpen() { - return true; + return isOpen; } @Override @@ -165,11 +267,20 @@ public void setClosedCallback(CompletedCallback handler) { public CompletedCallback getClosedCallback() { return closedCallback; } + + public void receiveHeaders(List
headers, HeadersMode headerMode) { + this.headers.setComplete(headers); + } } + final Settings okHttpSettings = new Settings(); + private int nextPingId; + private static final int OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024; + public AsyncSpdyConnection(AsyncSocket socket, Protocol protocol) { this.protocol = protocol; this.socket = socket; + this.bufferedSocket = new BufferedDataSink(socket); emitter = new BufferedDataEmitter(socket); emitter.setDataCallback(callback); @@ -179,24 +290,53 @@ public AsyncSpdyConnection(AsyncSocket socket, Protocol protocol) { else if (protocol == Protocol.HTTP_2) { variant = new Http20Draft13(); } - reader = variant.newReader(source, true); + reader = variant.newReader(bufferedSource = Okio.buffer(source), true); + writer = variant.newWriter(bufferedSink = Okio.buffer(sink), true); + + boolean client = true; + nextStreamId = client ? 1 : 2; + if (client && protocol == Protocol.HTTP_2) { + nextStreamId += 2; // In HTTP/2, 1 on client is reserved for Upgrade. + } + nextPingId = client ? 1 : 2; + // Flow control was designed more for servers, or proxies than edge clients. + // If we are a client, set the flow control window to 16MiB. This avoids + // thrashing window updates every 64KiB, yet small enough to avoid blowing + // up the heap. + if (client) { + okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, OKHTTP_CLIENT_WINDOW_SIZE); + } } DataCallback callback = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { - bb.get(source); - if (!reader.canProcessFrame(source)) - return; - try { - reader.nextFrame(AsyncSpdyConnection.this); - } - catch (IOException e) { - throw new AssertionError(e); + int needed; + while ((needed = reader.canProcessFrame(bb)) > 0) { + bb.get(source, needed); + try { + reader.nextFrame(AsyncSpdyConnection.this); + } + catch (IOException e) { + throw new AssertionError(e); + } } } }; + /** + * Sends a connection header if the current variant requires it. This should + * be called after {@link Builder#build} for all new connections. + */ + public void sendConnectionPreface() throws IOException { + writer.connectionPreface(); + writer.settings(okHttpSettings); + int windowSize = okHttpSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); + if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) { + writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE); + } + } + /** Even, positive numbered streams are pushed streams in HTTP/2. */ private boolean pushedStream(int streamId) { return protocol == Protocol.HTTP_2 && streamId != 0 && (streamId & 1) == 0; @@ -215,12 +355,16 @@ public void data(boolean inFinished, int streamId, BufferedSource source, int le source.skip(length); return; } - if (source != this.source) + if (source != this.bufferedSource || this.source.remaining() + source.buffer().size() != length) throw new AssertionError(); - this.source.get(socket.pending, length); + source.buffer().readAll(socket.pending); + this.source.get(socket.pending); + socket.updateWindowRead(length); Util.emitAllData(socket, socket.pending); if (inFinished) { - socket.report(null); + sockets.remove(streamId); + socket.close(); + Util.end(socket, null); } } @@ -228,7 +372,6 @@ public void data(boolean inFinished, int streamId, BufferedSource source, int le private int nextStreamId; @Override public void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, List
headerBlock, HeadersMode headersMode) { - /* if (pushedStream(streamId)) { throw new AssertionError("push"); // pushHeadersLater(streamId, headerBlock, inFinished); @@ -258,25 +401,34 @@ public void headers(boolean outFinished, boolean inFinished, int streamId, int a // If the stream ID is in the client's namespace, assume it's already closed. if (streamId % 2 == nextStreamId % 2) return; + throw new AssertionError("unexpected receive stream"); + // Create a stream. - socket = new SpdySocket(streamId, outFinished, inFinished, headerBlock); - lastGoodStreamId = streamId; - sockets.put(streamId, socket); - handler.receive(newStream); - return; +// socket = new SpdySocket(streamId, outFinished, inFinished, headerBlock); +// lastGoodStreamId = streamId; +// sockets.put(streamId, socket); +// handler.receive(newStream); +// return; } // The headers claim to be for a new stream, but we already have one. if (headersMode.failIfStreamPresent()) { - stream.closeLater(ErrorCode.PROTOCOL_ERROR); - removeStream(streamId); + try { + writer.rstStream(streamId, ErrorCode.INVALID_STREAM); + } + catch (IOException e) { + throw new AssertionError(e); + } + sockets.remove(streamId); return; } // Update an existing stream. - stream.receiveHeaders(headerBlock, headersMode); - if (inFinished) stream.receiveFin(); - */ + socket.receiveHeaders(headerBlock, headersMode); + if (inFinished) { + sockets.remove(streamId); + Util.end(socket, null); + } } @Override @@ -288,10 +440,11 @@ public void rstStream(int streamId, ErrorCode errorCode) { } SpdySocket rstStream = sockets.remove(streamId); if (rstStream != null) { - rstStream.report(new IOException(errorCode.toString())); + Util.end(rstStream, new IOException(errorCode.toString())); } } + long bytesLeftInWriteWindow; Settings peerSettings = new Settings(); private boolean receivedInitialPeerSettings = false; @Override @@ -310,7 +463,7 @@ public void settings(boolean clearPrevious, Settings settings) { if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) { delta = peerInitialWindowSize - priorWriteWindowSize; if (!receivedInitialPeerSettings) { - zero.addBytesToWriteWindow(delta); + addBytesToWriteWindow(delta); receivedInitialPeerSettings = true; } } @@ -319,8 +472,21 @@ public void settings(boolean clearPrevious, Settings settings) { } } + void addBytesToWriteWindow(long delta) { + bytesLeftInWriteWindow += delta; + for (SpdySocket socket: sockets.values()) { + Util.writable(socket); + } + } + @Override public void ackSettings() { + try { + writer.ackSettings(); + } + catch (IOException e) { + throw new AssertionError(e); + } } private Map pings; @@ -362,7 +528,7 @@ public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugDa Map.Entry entry = i.next(); int streamId = entry.getKey(); if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) { - entry.getValue().report(new IOException(ErrorCode.REFUSED_STREAM.toString())); + Util.end(entry.getValue(), new IOException(ErrorCode.REFUSED_STREAM.toString())); i.remove(); } } @@ -370,25 +536,25 @@ public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugDa @Override public void windowUpdate(int streamId, long windowSizeIncrement) { - System.out.println("fff"); - + if (streamId == 0) { + addBytesToWriteWindow(windowSizeIncrement); + return; + } + SpdySocket socket = sockets.get(streamId); + if (socket != null) + socket.addBytesToWriteWindow(windowSizeIncrement); } @Override public void priority(int streamId, int streamDependency, int weight, boolean exclusive) { - System.out.println("fff"); - } @Override public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) throws IOException { - System.out.println("fff"); - + throw new AssertionError("pushPromise"); } @Override public void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge) { - System.out.println("fff"); - } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSink.java new file mode 100644 index 000000000..aa9da9a15 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSink.java @@ -0,0 +1,52 @@ +package com.koushikdutta.async.http.spdy; + +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.http.spdy.okio.Segment; +import com.koushikdutta.async.http.spdy.okio.SegmentPool; +import com.koushikdutta.async.http.spdy.okio.Sink; +import com.koushikdutta.async.http.spdy.okio.Timeout; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Created by koush on 7/25/14. + */ +public class ByteBufferListSink extends ByteBufferList implements Sink { + @Override + public void write(Buffer source, long byteCount) throws IOException { + Segment s = source.head; + while (byteCount > 0) { + int toCopy = (int) Math.min(byteCount, s.limit - s.pos); + ByteBuffer b = obtain(toCopy); + b.put(s.data, s.pos, toCopy); + b.flip(); + add(b); + + s.pos += toCopy; + source.size -= toCopy; + byteCount -= toCopy; + + if (s.pos == s.limit) { + Segment toRecycle = s; + source.head = s = toRecycle.pop(); + SegmentPool.getInstance().recycle(toRecycle); + } + } + } + + @Override + public void flush() throws IOException { + } + + @Override + public Timeout timeout() { + return Timeout.NONE; + } + + @Override + public void close() throws IOException { + recycle(); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java index 5d57c772d..13803b3be 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java @@ -2,173 +2,39 @@ import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; -import com.koushikdutta.async.http.spdy.okio.ByteString; -import com.koushikdutta.async.http.spdy.okio.Sink; +import com.koushikdutta.async.http.spdy.okio.Source; import com.koushikdutta.async.http.spdy.okio.Timeout; -import com.koushikdutta.async.util.Charsets; import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteOrder; -import java.nio.charset.Charset; +import java.nio.ByteBuffer; /** - * Created by koush on 7/17/14. + * Created by koush on 7/25/14. */ -public class ByteBufferListSource extends ByteBufferList implements BufferedSource { - @Override - public Buffer buffer() { - return null; - } - - @Override - public boolean exhausted() throws IOException { - return !hasRemaining(); - } - - @Override - public void require(long byteCount) throws IOException { - if (remaining() < byteCount) - throw new AssertionError("out of data"); - } - - @Override - public byte readByte() throws IOException { - return order(ByteOrder.BIG_ENDIAN).get(); - } - - @Override - public short readShort() throws IOException { - return order(ByteOrder.BIG_ENDIAN).getShort(); - } - - @Override - public short readShortLe() throws IOException { - return order(ByteOrder.LITTLE_ENDIAN).getShort(); - } - - @Override - public int readInt() throws IOException { - return order(ByteOrder.BIG_ENDIAN).getInt(); - } - - @Override - public int readIntLe() throws IOException { - return order(ByteOrder.LITTLE_ENDIAN).getInt(); - } - - @Override - public long readLong() throws IOException { - return order(ByteOrder.BIG_ENDIAN).getLong(); - } - - @Override - public long readLongLe() throws IOException { - return order(ByteOrder.LITTLE_ENDIAN).getLong(); - } - - @Override - public void skip(long byteCount) throws IOException { - if (byteCount > Integer.MAX_VALUE) - throw new AssertionError("too much skippy, use less peanut butter"); - read(new byte[(int)byteCount]); - } - - @Override - public ByteString readByteString() throws IOException { - return readByteString(remaining()); - } - - @Override - public ByteString readByteString(long byteCount) throws IOException { - return ByteString.of(readByteArray(byteCount)); - } - - @Override - public byte[] readByteArray() throws IOException { - return getAllByteArray(); - } - - @Override - public byte[] readByteArray(long byteCount) throws IOException { - byte[] ret = new byte[(int)byteCount]; - get(ret); - return ret; - } - - @Override - public int read(byte[] sink) throws IOException { - return read(sink, 0, sink.length); - } - - @Override - public void readFully(byte[] sink) throws IOException { - read(sink, 0, sink.length); - } - - @Override - public int read(byte[] sink, int offset, int byteCount) throws IOException { - get(sink, offset, byteCount); - return byteCount; - } - - @Override - public void readFully(Buffer sink, long byteCount) throws IOException { - throw new AssertionError("not implemented"); - } - - @Override - public long readAll(Sink sink) throws IOException { - throw new AssertionError("not implemented"); - } - - @Override - public String readUtf8() throws IOException { - return readUtf8(remaining()); - } - - @Override - public String readUtf8(long byteCount) throws IOException { - return new String(readByteArray(byteCount), Charsets.UTF_8); - } - - @Override - public String readUtf8Line() throws IOException { - throw new AssertionError("not implemented"); - } - - @Override - public String readUtf8LineStrict() throws IOException { - throw new AssertionError("not implemented"); - } - - @Override - public String readString(long byteCount, Charset charset) throws IOException { - return new String(readByteArray(byteCount), charset); - } - - @Override - public long indexOf(byte b) throws IOException { - throw new AssertionError("not implemented"); - } - - @Override - public InputStream inputStream() { - throw new AssertionError("not implemented"); - } - +public class ByteBufferListSource extends ByteBufferList implements Source { @Override public long read(Buffer sink, long byteCount) throws IOException { - throw new AssertionError("not implemented"); + if (!hasRemaining()) + throw new AssertionError("empty!"); + int total = 0; + while (total < byteCount && hasRemaining()) { + ByteBuffer b = remove(); + int toRead = (int)Math.min(byteCount - total, b.remaining()); + total += toRead; + sink.write(b.array(), b.arrayOffset() + b.position(), toRead); + b.position(b.position() + toRead); + addFirst(b); + } + return total; } @Override public Timeout timeout() { - return null; + return Timeout.NONE; } @Override public void close() throws IOException { + recycle(); } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index 35d425782..3ebe80226 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -1,19 +1,32 @@ package com.koushikdutta.async.http.spdy; +import android.net.Uri; + import com.koushikdutta.async.AsyncSSLSocket; import com.koushikdutta.async.AsyncSSLSocketWrapper; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.future.Cancellable; +import com.koushikdutta.async.future.FutureCallback; +import com.koushikdutta.async.future.SimpleCancellable; +import com.koushikdutta.async.future.TransformFuture; import com.koushikdutta.async.http.AsyncHttpClient; +import com.koushikdutta.async.http.AsyncHttpClientMiddleware; import com.koushikdutta.async.http.AsyncSSLEngineConfigurator; import com.koushikdutta.async.http.AsyncSSLSocketMiddleware; +import com.koushikdutta.async.http.Headers; +import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.Protocol; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.SpdyConnection; import com.koushikdutta.async.util.Charsets; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -41,8 +54,15 @@ static byte[] concatLengthPrefixed(Protocol... protocols) { return ret; } + private static String requestPath(Uri uri) { + String pathAndQuery = uri.getPath(); + if (pathAndQuery == null) return "/"; + if (!pathAndQuery.startsWith("/")) return "/" + pathAndQuery; + return pathAndQuery; + } + @Override - protected AsyncSSLSocketWrapper.HandshakeCallback createHandshakeCallback(final ConnectCallback callback) { + protected AsyncSSLSocketWrapper.HandshakeCallback createHandshakeCallback(final GetSocketData data, final ConnectCallback callback) { return new AsyncSSLSocketWrapper.HandshakeCallback() { @Override public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { @@ -53,8 +73,23 @@ public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { try { long ptr = (Long)sslNativePointer.get(socket.getSSLEngine()); byte[] proto = (byte[])nativeGetAlpnNegotiatedProtocol.invoke(null, ptr); + if (proto == null) { + callback.onConnectCompleted(null, socket); + return; + } String protoString = new String(proto); - AsyncSpdyConnection connection = new AsyncSpdyConnection(socket, Protocol.get(protoString)); + Protocol p = Protocol.get(protoString); + if (p == null) { + callback.onConnectCompleted(null, socket); + return; + } + final AsyncSpdyConnection connection = new AsyncSpdyConnection(socket, Protocol.get(protoString)); + connection.sendConnectionPreface(); + connection.flush(); + + connections.put(data.request.getUri().getHost(), connection); + + newSocket(data, connection, callback); } catch (Exception ex) { socket.close(); @@ -64,6 +99,83 @@ public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { }; } + private void newSocket(GetSocketData data, final AsyncSpdyConnection connection, final ConnectCallback callback) { + final ArrayList
headers = new ArrayList
(); + headers.add(new Header(Header.TARGET_METHOD, data.request.getMethod())); + headers.add(new Header(Header.TARGET_PATH, requestPath(data.request.getUri()))); + String host = data.request.getHeaders().get("Host"); + if (Protocol.SPDY_3 == connection.protocol) { + headers.add(new Header(Header.VERSION, "HTTP/1.1")); + headers.add(new Header(Header.TARGET_HOST, host)); + } else if (Protocol.HTTP_2 == connection.protocol) { + headers.add(new Header(Header.TARGET_AUTHORITY, host)); // Optional in HTTP/2 + } else { + throw new AssertionError(); + } + headers.add(new Header(Header.TARGET_SCHEME, data.request.getUri().getScheme())); + + Multimap mm = data.request.getHeaders().getMultiMap(); + for (String key: mm.keySet()) { + if (SpdyTransport.isProhibitedHeader(connection.protocol, key)) + continue; + for (String value: mm.get(key)) { + headers.add(new Header(key.toLowerCase(), value)); + } + } + + connection.socket.getServer().postDelayed(new Runnable() { + @Override + public void run() { + try { + AsyncSpdyConnection.SpdySocket spdy = connection.newStream(headers, false, true); + connection.flush(); + callback.onConnectCompleted(null, spdy); + } + catch (Exception e) { + throw new AssertionError(e); + } + } + }, 1000); + } + + @Override + public boolean exchangeHeaders(final ExchangeHeaderData data) { + if (!(data.socket instanceof AsyncSpdyConnection.SpdySocket)) + return false; + + // headers were already sent as part of the socket being opened. + data.sendHeadersCallback.onCompleted(null); + + final AsyncSpdyConnection.SpdySocket spdySocket = (AsyncSpdyConnection.SpdySocket)data.socket; + spdySocket.headers() + .then(new TransformFuture>() { + @Override + protected void transform(List
result) throws Exception { + Headers headers = new Headers(); + for (Header header: result) { + String key = header.name.utf8(); + String value = header.value.utf8(); + headers.add(key, value); + } + String status = headers.remove(Header.RESPONSE_STATUS.utf8()); + String[] statusParts = status.split(" ", 2); + data.response.code(Integer.parseInt(statusParts[0])); + data.response.message(statusParts[1]); + data.response.protocol(headers.remove(Header.VERSION.utf8())); + data.response.headers(headers); + setComplete(headers); + } + }) + .setCallback(new FutureCallback() { + @Override + public void onCompleted(Exception e, Headers result) { + data.receiveHeadersCallback.onCompleted(e); + data.response.emitter(spdySocket); + } + }); + return true; + } + private void configure(SSLEngine engine, String host, int port) { if (!initialized) { initialized = true; @@ -144,6 +256,7 @@ protected SSLEngine createConfiguredSSLEngine(String host, int port) { Field useSni; Method nativeGetNpnNegotiatedProtocol; Method nativeGetAlpnNegotiatedProtocol; + Hashtable connections = new Hashtable(); @Override public void setSSLContext(SSLContext sslContext) { @@ -153,6 +266,24 @@ public void setSSLContext(SSLContext sslContext) { @Override public Cancellable getSocket(GetSocketData data) { - return super.getSocket(data); + final Uri uri = data.request.getUri(); + final int port = getSchemePort(data.request.getUri()); + if (port == -1) { + return null; + } + + // can we use an existing connection to satisfy this, or do we need a new one? + String host = uri.getHost(); + AsyncSpdyConnection conn = connections.get(host); + if (conn == null || !conn.socket.isOpen()) { + connections.remove(host); + return super.getSocket(data); + } + + newSocket(data, conn, data.connectCallback); + + SimpleCancellable ret = new SimpleCancellable(); + ret.setComplete(); + return ret; } } \ No newline at end of file diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java new file mode 100644 index 000000000..e915a0650 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.koushikdutta.async.http.spdy; + + +import com.koushikdutta.async.http.Protocol; +import com.koushikdutta.async.http.spdy.okhttp.internal.Util; +import com.koushikdutta.async.http.spdy.okio.ByteString; + +import java.util.List; + + +final class SpdyTransport { + /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */ + private static final List SPDY_3_PROHIBITED_HEADERS = Util.immutableList( + "accept-encoding", + "user-agent", + "accept", + + "connection", + "host", + "keep-alive", + "proxy-connection", + "transfer-encoding"); + + /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */ + private static final List HTTP_2_PROHIBITED_HEADERS = Util.immutableList( + "connection", + "host", + "keep-alive", + "proxy-connection", + "te", + "transfer-encoding", + "encoding", + "upgrade"); + + /** When true, this header should not be emitted or consumed. */ + static boolean isProhibitedHeader(Protocol protocol, String name) { + if (protocol == Protocol.SPDY_3) { + return SPDY_3_PROHIBITED_HEADERS.contains(name.toLowerCase()); + } else if (protocol == Protocol.HTTP_2) { + return HTTP_2_PROHIBITED_HEADERS.contains(name.toLowerCase()); + } else { + throw new AssertionError(protocol); + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java index 5305f632e..19b6b77a0 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java @@ -26,7 +26,7 @@ /** Reads transport frames for SPDY/3 or HTTP/2. */ public interface FrameReader extends Closeable { - boolean canProcessFrame(ByteBufferList bb); + int canProcessFrame(ByteBufferList bb); void readConnectionPreface() throws IOException; boolean nextFrame(Handler handler) throws IOException; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java index 88958ef15..1425cbb5d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java @@ -96,14 +96,16 @@ static final class Reader implements FrameReader { final HpackDraft08.Reader hpackReader; @Override - public boolean canProcessFrame(ByteBufferList bb) { + public int canProcessFrame(ByteBufferList bb) { if (bb.remaining() < 4) - return false; + return 0; bb.order(ByteOrder.BIG_ENDIAN); int w1 = bb.peekInt(); short length = (short) ((w1 & 0x3fff0000) >> 16); // 14-bit unsigned == MAX_FRAME_SIZE - return bb.remaining() >= 8 + length; + if (bb.remaining() < 8 + length) + return 0; + return 8 + length; } Reader(BufferedSource source, int headerTableSize, boolean client) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java index 4b332f6c9..1b96f64a5 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java @@ -53,7 +53,7 @@ public final class Settings { /** spdy/3: Retransmission rate. Percentage */ static final int DOWNLOAD_RETRANS_RATE = 6; /** Window size in bytes. */ - static final int INITIAL_WINDOW_SIZE = 7; + public static final int INITIAL_WINDOW_SIZE = 7; /** spdy/3: Window size in bytes. */ static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 8; /** Flow control options. */ @@ -82,7 +82,7 @@ public void clear() { Arrays.fill(values, 0); } - Settings set(int id, int idFlags, int value) { + public Settings set(int id, int idFlags, int value) { if (id >= values.length) { return this; // Discard unknown settings. } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java index 72a3df2ba..fe1993528 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java @@ -128,15 +128,17 @@ static final class Reader implements FrameReader { } @Override - public boolean canProcessFrame(ByteBufferList bb) { - if (bb.remaining() < 8) - return false; + public int canProcessFrame(ByteBufferList bb) { + if (source.buffer().size() + bb.remaining() < 8) + return 0; ByteBuffer peek = ByteBuffer.wrap(bb.peekBytes(8)).order(ByteOrder.BIG_ENDIAN); - peek.getInt(); + int w1 = peek.getInt(); int w2 = peek.getInt(); int length = (w2 & 0xffffff); - return bb.remaining() >= 8 + length; + if (bb.remaining() < 8 + length) + return 0; + return 8 + length; } /** diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java index 4ac22b15e..bb6852dc4 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java @@ -45,8 +45,8 @@ * This class avoids zero-fill and GC churn by pooling byte arrays. */ public final class Buffer implements BufferedSource, BufferedSink, Cloneable { - Segment head; - long size; + public Segment head; + public long size; public Buffer() { } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java index ce50796f3..76f7cc031 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java @@ -25,99 +25,99 @@ * to decompress data read from another source. */ public final class InflaterSource implements Source { - private final BufferedSource source; - private final Inflater inflater; + private final BufferedSource source; + private final Inflater inflater; - /** - * When we call Inflater.setInput(), the inflater keeps our byte array until - * it needs input again. This tracks how many bytes the inflater is currently - * holding on to. - */ - private int bufferBytesHeldByInflater; - private boolean closed; + /** + * When we call Inflater.setInput(), the inflater keeps our byte array until + * it needs input again. This tracks how many bytes the inflater is currently + * holding on to. + */ + private int bufferBytesHeldByInflater; + private boolean closed; - public InflaterSource(Source source, Inflater inflater) { - this(Okio.buffer(source), inflater); - } + public InflaterSource(Source source, Inflater inflater) { + this(Okio.buffer(source), inflater); + } - /** - * This package-private constructor shares a buffer with its trusted caller. - * In general we can't share a BufferedSource because the inflater holds input - * bytes until they are inflated. - */ - InflaterSource(BufferedSource source, Inflater inflater) { - if (source == null) throw new IllegalArgumentException("source == null"); - if (inflater == null) throw new IllegalArgumentException("inflater == null"); - this.source = source; - this.inflater = inflater; - } + /** + * This package-private constructor shares a buffer with its trusted caller. + * In general we can't share a BufferedSource because the inflater holds input + * bytes until they are inflated. + */ + InflaterSource(BufferedSource source, Inflater inflater) { + if (source == null) throw new IllegalArgumentException("source == null"); + if (inflater == null) throw new IllegalArgumentException("inflater == null"); + this.source = source; + this.inflater = inflater; + } - @Override public long read( - Buffer sink, long byteCount) throws IOException { - if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); - if (closed) throw new IllegalStateException("closed"); - if (byteCount == 0) return 0; + @Override public long read( + Buffer sink, long byteCount) throws IOException { + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + if (closed) throw new IllegalStateException("closed"); + if (byteCount == 0) return 0; - while (true) { - boolean sourceExhausted = refill(); + while (true) { + boolean sourceExhausted = refill(); - // Decompress the inflater's compressed data into the sink. - try { - Segment tail = sink.writableSegment(1); - int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit); - if (bytesInflated > 0) { - tail.limit += bytesInflated; - sink.size += bytesInflated; - return bytesInflated; + // Decompress the inflater's compressed data into the sink. + try { + Segment tail = sink.writableSegment(1); + int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit); + if (bytesInflated > 0) { + tail.limit += bytesInflated; + sink.size += bytesInflated; + return bytesInflated; + } + if (inflater.finished() || inflater.needsDictionary()) { + releaseInflatedBytes(); + return -1; + } + if (sourceExhausted) throw new EOFException("source exhausted prematurely"); + } catch (DataFormatException e) { + throw new IOException(e); + } } - if (inflater.finished() || inflater.needsDictionary()) { - releaseInflatedBytes(); - return -1; - } - if (sourceExhausted) throw new EOFException("source exhausted prematurely"); - } catch (DataFormatException e) { - throw new IOException(e); - } } - } - /** - * Refills the inflater with compressed data if it needs input. (And only if - * it needs input). Returns true if the inflater required input but the source - * was exhausted. - */ - public boolean refill() throws IOException { - if (!inflater.needsInput()) return false; + /** + * Refills the inflater with compressed data if it needs input. (And only if + * it needs input). Returns true if the inflater required input but the source + * was exhausted. + */ + public boolean refill() throws IOException { + if (!inflater.needsInput()) return false; - releaseInflatedBytes(); - if (inflater.getRemaining() != 0) throw new IllegalStateException("?"); // TODO: possible? + releaseInflatedBytes(); + if (inflater.getRemaining() != 0) throw new IllegalStateException("?"); // TODO: possible? - // If there are compressed bytes in the source, assign them to the inflater. - if (source.exhausted()) return true; + // If there are compressed bytes in the source, assign them to the inflater. + if (source.exhausted()) return true; - // Assign buffer bytes to the inflater. - byte[] data = source.readByteArray(); - bufferBytesHeldByInflater = data.length; - inflater.setInput(data, 0, bufferBytesHeldByInflater); - return false; - } + // Assign buffer bytes to the inflater. + Segment head = source.buffer().head; + bufferBytesHeldByInflater = head.limit - head.pos; + inflater.setInput(head.data, head.pos, bufferBytesHeldByInflater); + return false; + } - /** When the inflater has processed compressed data, remove it from the buffer. */ - private void releaseInflatedBytes() throws IOException { - if (bufferBytesHeldByInflater == 0) return; - int toRelease = bufferBytesHeldByInflater - inflater.getRemaining(); - bufferBytesHeldByInflater -= toRelease; - source.skip(toRelease); - } + /** When the inflater has processed compressed data, remove it from the buffer. */ + private void releaseInflatedBytes() throws IOException { + if (bufferBytesHeldByInflater == 0) return; + int toRelease = bufferBytesHeldByInflater - inflater.getRemaining(); + bufferBytesHeldByInflater -= toRelease; + source.skip(toRelease); + } - @Override public Timeout timeout() { - return source.timeout(); - } + @Override public Timeout timeout() { + return source.timeout(); + } - @Override public void close() throws IOException { - if (closed) return; - inflater.end(); - closed = true; - source.close(); - } + @Override public void close() throws IOException { + if (closed) return; + inflater.end(); + closed = true; + source.close(); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java index 501343a2d..9c289ef41 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java @@ -31,13 +31,13 @@ public final class Segment { // TODO: Is 2 KiB a good default segment size? static final int SIZE = 2048; - final byte[] data = new byte[SIZE]; + public final byte[] data = new byte[SIZE]; /** The next byte of application data byte to read in this segment. */ - int pos; + public int pos; /** The first byte of available data ready to be written to. */ - int limit; + public int limit; /** Next segment in a linked or circularly-linked list. */ Segment next; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java index f410c8c23..58d362b90 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java @@ -51,7 +51,7 @@ Segment take() { return new Segment(); // Pool is empty. Don't zero-fill while holding a lock. } - void recycle(Segment segment) { + public void recycle(Segment segment) { if (segment.next != null || segment.prev != null) throw new IllegalArgumentException(); synchronized (this) { if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full. From e1531ed2de6fcb6215d384b1547ac1d8b167997a Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sun, 27 Jul 2014 12:14:33 -0700 Subject: [PATCH 048/399] remove exception throwing. --- .../async/http/spdy/AsyncSpdyConnection.java | 35 +++++++++++-------- .../async/http/spdy/SpdyMiddleware.java | 17 +++------ 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index 066f92a18..7471615ad 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -74,19 +74,19 @@ public void flush() { * @param in true to create an input stream that the remote peer can use to send data to us. * Corresponds to {@code FLAG_UNIDIRECTIONAL}. */ - public SpdySocket newStream(List
requestHeaders, boolean out, boolean in) throws IOException { + public SpdySocket newStream(List
requestHeaders, boolean out, boolean in) { return newStream(0, requestHeaders, out, in); } private SpdySocket newStream(int associatedStreamId, List
requestHeaders, boolean out, - boolean in) throws IOException { + boolean in) { boolean outFinished = !out; boolean inFinished = !in; SpdySocket socket; int streamId; if (shutdown) { - throw new IOException("shutdown"); + return null; } streamId = nextStreamId; @@ -96,20 +96,25 @@ private SpdySocket newStream(int associatedStreamId, List
requestHeaders sockets.put(streamId, socket); // setIdle(false); } - if (associatedStreamId == 0) { - writer.synStream(outFinished, inFinished, streamId, associatedStreamId, - requestHeaders); - } else if (client) { - throw new IllegalArgumentException("client streams shouldn't have associated stream IDs"); - } else { // HTTP/2 has a PUSH_PROMISE frame. - writer.pushPromise(associatedStreamId, streamId, requestHeaders); - } + try { + if (associatedStreamId == 0) { + writer.synStream(outFinished, inFinished, streamId, associatedStreamId, + requestHeaders); + } else if (client) { + throw new IllegalArgumentException("client streams shouldn't have associated stream IDs"); + } else { // HTTP/2 has a PUSH_PROMISE frame. + writer.pushPromise(associatedStreamId, streamId, requestHeaders); + } - if (!out) { - writer.flush(); - } + if (!out) { + writer.flush(); + } - return socket; + return socket; + } + catch (IOException e) { + throw new AssertionError(e); + } } int totalWindowRead; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index 3ebe80226..83627c5c8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -100,6 +100,7 @@ public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { } private void newSocket(GetSocketData data, final AsyncSpdyConnection connection, final ConnectCallback callback) { + data.request.logv("using spdy connection"); final ArrayList
headers = new ArrayList
(); headers.add(new Header(Header.TARGET_METHOD, data.request.getMethod())); headers.add(new Header(Header.TARGET_PATH, requestPath(data.request.getUri()))); @@ -123,19 +124,9 @@ private void newSocket(GetSocketData data, final AsyncSpdyConnection connection, } } - connection.socket.getServer().postDelayed(new Runnable() { - @Override - public void run() { - try { - AsyncSpdyConnection.SpdySocket spdy = connection.newStream(headers, false, true); - connection.flush(); - callback.onConnectCompleted(null, spdy); - } - catch (Exception e) { - throw new AssertionError(e); - } - } - }, 1000); + AsyncSpdyConnection.SpdySocket spdy = connection.newStream(headers, false, true); + connection.flush(); + callback.onConnectCompleted(null, spdy); } @Override From 9e24bdf9a04b2d664424255bfba28965598a69cc Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sun, 27 Jul 2014 12:21:15 -0700 Subject: [PATCH 049/399] remove leftover okhttp cruft. --- .../async/http/spdy/SpdyMiddleware.java | 2 - .../spdy/okhttp/internal/NamedRunnable.java | 40 - .../async/http/spdy/okhttp/internal/Util.java | 160 ---- .../internal/spdy/IncomingStreamHandler.java | 36 - .../http/spdy/okhttp/internal/spdy/Spdy3.java | 3 +- .../okhttp/internal/spdy/SpdyConnection.java | 874 ------------------ .../spdy/okhttp/internal/spdy/SpdyStream.java | 577 ------------ 7 files changed, 2 insertions(+), 1690 deletions(-) delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/NamedRunnable.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/IncomingStreamHandler.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyConnection.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyStream.java diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index 83627c5c8..0e09fdd79 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -11,14 +11,12 @@ import com.koushikdutta.async.future.SimpleCancellable; import com.koushikdutta.async.future.TransformFuture; import com.koushikdutta.async.http.AsyncHttpClient; -import com.koushikdutta.async.http.AsyncHttpClientMiddleware; import com.koushikdutta.async.http.AsyncSSLEngineConfigurator; import com.koushikdutta.async.http.AsyncSSLSocketMiddleware; import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.SpdyConnection; import com.koushikdutta.async.util.Charsets; import java.lang.reflect.Field; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/NamedRunnable.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/NamedRunnable.java deleted file mode 100644 index 9d9555162..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/NamedRunnable.java +++ /dev/null @@ -1,40 +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.koushikdutta.async.http.spdy.okhttp.internal; - -/** - * Runnable implementation which always sets its thread name. - */ -public abstract class NamedRunnable implements Runnable { - private final String name; - - public NamedRunnable(String format, Object... args) { - this.name = String.format(format, args); - } - - @Override public final void run() { - String oldName = Thread.currentThread().getName(); - Thread.currentThread().setName(name); - try { - execute(); - } finally { - Thread.currentThread().setName(oldName); - } - } - - protected abstract void execute(); -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java index a6e00f45a..63f2661aa 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java @@ -16,118 +16,21 @@ package com.koushikdutta.async.http.spdy.okhttp.internal; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; -import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.ByteString; -import com.koushikdutta.async.http.spdy.okio.Source; - import java.io.Closeable; -import java.io.File; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.URI; -import java.net.URL; -import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.ThreadFactory; - -import static java.util.concurrent.TimeUnit.NANOSECONDS; /** Junk drawer of utility methods. */ public final class Util { - public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - public static final String[] EMPTY_STRING_ARRAY = new String[0]; - - /** A cheap and type-safe constant for the US-ASCII Charset. */ - public static final Charset US_ASCII = Charset.forName("US-ASCII"); - - /** A cheap and type-safe constant for the UTF-8 Charset. */ - public static final Charset UTF_8 = Charset.forName("UTF-8"); - - private Util() { - } - - public static int getEffectivePort(URI uri) { - return getEffectivePort(uri.getScheme(), uri.getPort()); - } - - public static int getEffectivePort(URL url) { - return getEffectivePort(url.getProtocol(), url.getPort()); - } - - private static int getEffectivePort(String scheme, int specifiedPort) { - return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme); - } - - public static int getDefaultPort(String protocol) { - if ("http".equals(protocol)) return 80; - if ("https".equals(protocol)) return 443; - return -1; - } - public static void checkOffsetAndCount(long arrayLength, long offset, long count) { if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) { throw new ArrayIndexOutOfBoundsException(); } } - /** Returns true if two possibly-null objects are equal. */ - public static boolean equal(Object a, Object b) { - return a == b || (a != null && a.equals(b)); - } - - /** - * Closes {@code closeable}, ignoring any checked exceptions. Does nothing - * if {@code closeable} is null. - */ - public static void closeQuietly(Closeable closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (RuntimeException rethrown) { - throw rethrown; - } catch (Exception ignored) { - } - } - } - - /** - * Closes {@code socket}, ignoring any checked exceptions. Does nothing if - * {@code socket} is null. - */ - public static void closeQuietly(Socket socket) { - if (socket != null) { - try { - socket.close(); - } catch (RuntimeException rethrown) { - throw rethrown; - } catch (Exception ignored) { - } - } - } - - /** - * Closes {@code 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. @@ -151,51 +54,6 @@ public static void closeAll(Closeable a, Closeable b) throws IOException { throw new AssertionError(thrown); } - /** - * Deletes the contents of {@code dir}. Throws an IOException if any file - * could not be deleted, or if {@code dir} is not a readable directory. - */ - public static void deleteContents(File dir) throws IOException { - File[] files = dir.listFiles(); - if (files == null) { - throw new IOException("not a readable directory: " + dir); - } - for (File file : files) { - if (file.isDirectory()) { - deleteContents(file); - } - if (!file.delete()) { - throw new IOException("failed to delete file: " + file); - } - } - } - - /** Reads until {@code in} is exhausted or the timeout has elapsed. */ - public static boolean skipAll(Source in, int timeoutMillis) throws IOException { - // TODO: Implement deadlines everywhere so they can do this work. - long startNanos = System.nanoTime(); - Buffer skipBuffer = new Buffer(); - while (NANOSECONDS.toMillis(System.nanoTime() - startNanos) < timeoutMillis) { - long read = in.read(skipBuffer, 2048); - if (read == -1) return true; // Successfully exhausted the stream. - skipBuffer.clear(); - } - return false; // Ran out of time. - } - - /** 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 ByteString.of(md5bytes).hex(); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } - } - /** Returns an immutable copy of {@code list}. */ public static List immutableList(List list) { return Collections.unmodifiableList(new ArrayList(list)); @@ -205,22 +63,4 @@ public static List immutableList(List list) { public static List immutableList(T... elements) { return Collections.unmodifiableList(Arrays.asList(elements.clone())); } - - public static ThreadFactory threadFactory(final String name, final boolean daemon) { - return new ThreadFactory() { - @Override public Thread newThread(Runnable runnable) { - Thread result = new Thread(runnable, name); - result.setDaemon(daemon); - return result; - } - }; - } - - public static List
headerEntries(String... elements) { - List
result = new ArrayList
(elements.length / 2); - for (int i = 0; i < elements.length; i += 2) { - result.add(new Header(elements[i], elements[i + 1])); - } - return result; - } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/IncomingStreamHandler.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/IncomingStreamHandler.java deleted file mode 100644 index d36799f78..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/IncomingStreamHandler.java +++ /dev/null @@ -1,36 +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.koushikdutta.async.http.spdy.okhttp.internal.spdy; - -import java.io.IOException; - -/** Listener to be notified when a connected peer creates a new stream. */ -public interface IncomingStreamHandler { - IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() { - @Override public void receive(SpdyStream stream) throws IOException { - stream.close(ErrorCode.REFUSED_STREAM); - } - }; - - /** - * Handle a new stream from this connection's peer. Implementations should - * respond by either {@link SpdyStream#reply replying to the stream} or - * {@link SpdyStream#close closing it}. This response does not need to be - * synchronous. - */ - void receive(SpdyStream stream) throws IOException; -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java index fe1993528..e38f3f01b 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java @@ -24,6 +24,7 @@ import com.koushikdutta.async.http.spdy.okio.ByteString; import com.koushikdutta.async.http.spdy.okio.DeflaterSink; import com.koushikdutta.async.http.spdy.okio.Okio; +import com.koushikdutta.async.util.Charsets; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -94,7 +95,7 @@ public final class Spdy3 implements Variant { + "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()); + + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Charsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { throw new AssertionError(); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyConnection.java deleted file mode 100644 index 52f924f14..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyConnection.java +++ /dev/null @@ -1,874 +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.koushikdutta.async.http.spdy.okhttp.internal.spdy; - -import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.http.spdy.okhttp.internal.NamedRunnable; -import com.koushikdutta.async.http.spdy.okhttp.internal.Util; -import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; -import com.koushikdutta.async.http.spdy.okio.ByteString; -import com.koushikdutta.async.http.spdy.okio.Okio; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - - -import static com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE; - -/** - * A socket connection to a remote peer. A connection hosts streams which can - * send and receive data. - * - *

Many methods in this API are synchronous: the call is - * completed before the method returns. This is typical for Java but atypical - * for SPDY. This is motivated by exception transparency: an IOException that - * was triggered by a certain caller can be caught and handled by that caller. - */ -public final class SpdyConnection implements Closeable { - - // Internal state of this connection is guarded by 'this'. No blocking - // operations may be performed while holding this lock! - // - // Socket writes are guarded by frameWriter. - // - // Socket reads are unguarded but are only made by the reader thread. - // - // Certain operations (like SYN_STREAM) need to synchronize on both the - // 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'. - - private static final ExecutorService executor = new ThreadPoolExecutor(0, - Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue(), - Util.threadFactory("OkHttp SpdyConnection", true)); - - /** The protocol variant, like {@link com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3}. */ - final Protocol protocol; - - /** True if this peer initiated the connection. */ - final boolean client; - - /** - * User code to run in response to an incoming stream. Callbacks must not be - * run on the callback executor. - */ - private final IncomingStreamHandler handler; - private final Map streams = new HashMap(); - private final String hostName; - private int lastGoodStreamId; - private int nextStreamId; - private boolean shutdown; - private long idleStartTimeNs = System.nanoTime(); - - /** Ensures push promise callbacks events are sent in order per stream. */ - private final ExecutorService pushExecutor; - - /** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */ - private Map pings; - /** User code to run in response to push promise events. */ - private final PushObserver pushObserver; - private int nextPingId; - - /** - * The total number of bytes consumed by the application, but not yet - * acknowledged by sending a {@code WINDOW_UPDATE} frame on this connection. - */ - // Visible for testing - long unacknowledgedBytesRead = 0; - - /** - * Count of bytes that can be written on the connection before receiving a - * window update. - */ - // Visible for testing - long bytesLeftInWriteWindow; - - /** Settings we communicate to the peer. */ - // TODO: Do we want to dynamically adjust settings, or KISS and only set once? - final Settings okHttpSettings = new Settings(); - // okHttpSettings.set(Settings.MAX_CONCURRENT_STREAMS, 0, max); - private static final int OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024; - - /** Settings we receive from the peer. */ - // TODO: MWS will need to guard on this setting before attempting to push. - final Settings peerSettings = new Settings(); - - private boolean receivedInitialPeerSettings = false; - final Variant variant; - final Socket socket; - final FrameWriter frameWriter; - final long maxFrameSize; - - // Visible for testing - final Reader readerRunnable; - - private SpdyConnection(Builder builder) throws IOException { - protocol = builder.protocol; - pushObserver = builder.pushObserver; - client = builder.client; - handler = builder.handler; - // http://tools.ietf.org/html/draft-ietf-httpbis-http2-13#section-5.1.1 - nextStreamId = builder.client ? 1 : 2; - if (builder.client && protocol == Protocol.HTTP_2) { - nextStreamId += 2; // In HTTP/2, 1 on client is reserved for Upgrade. - } - - nextPingId = builder.client ? 1 : 2; - - // Flow control was designed more for servers, or proxies than edge clients. - // If we are a client, set the flow control window to 16MiB. This avoids - // thrashing window updates every 64KiB, yet small enough to avoid blowing - // up the heap. - if (builder.client) { - okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, OKHTTP_CLIENT_WINDOW_SIZE); - } - - hostName = builder.hostName; - - if (protocol == Protocol.HTTP_2) { - variant = new Http20Draft13(); - // Like newSingleThreadExecutor, except lazy creates the thread. - pushExecutor = new ThreadPoolExecutor(0, 1, - 0L, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - Util.threadFactory(String.format("OkHttp %s Push Observer", hostName), true)); - // 1 less than SPDY http://tools.ietf.org/html/draft-ietf-httpbis-http2-13#section-6.9.2 - peerSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, 65535); - } else if (protocol == Protocol.SPDY_3) { - variant = new Spdy3(); - pushExecutor = null; - } else { - throw new AssertionError(protocol); - } - bytesLeftInWriteWindow = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); - socket = builder.socket; - frameWriter = variant.newWriter(Okio.buffer(Okio.sink(builder.socket)), client); - maxFrameSize = variant.maxFrameSize(); - - readerRunnable = new Reader(); - new Thread(readerRunnable).start(); // Not a daemon thread. - } - - /** The protocol as selected using NPN or ALPN. */ - public Protocol getProtocol() { - return protocol; - } - - /** - * Returns the number of {@link SpdyStream#isOpen() open streams} on this - * connection. - */ - public synchronized int openStreamCount() { - return streams.size(); - } - - synchronized SpdyStream getStream(int id) { - return streams.get(id); - } - - synchronized SpdyStream removeStream(int streamId) { - SpdyStream stream = streams.remove(streamId); - if (stream != null && streams.isEmpty()) { - setIdle(true); - } - return stream; - } - - private synchronized void setIdle(boolean value) { - idleStartTimeNs = value ? System.nanoTime() : Long.MAX_VALUE; - } - - /** Returns true if this connection is idle. */ - public synchronized boolean isIdle() { - return idleStartTimeNs != Long.MAX_VALUE; - } - - /** - * 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; - } - - /** - * Returns a new server-initiated stream. - * - * @param associatedStreamId the stream that triggered the sender to create - * this stream. - * @param out true to create an output stream that we can use to send data - * to the remote peer. Corresponds to {@code FLAG_FIN}. - */ - public SpdyStream pushStream(int associatedStreamId, List

requestHeaders, boolean out) - throws IOException { - if (client) throw new IllegalStateException("Client cannot push requests."); - if (protocol != Protocol.HTTP_2) throw new IllegalStateException("protocol != HTTP_2"); - return newStream(associatedStreamId, requestHeaders, out, false); - } - - /** - * Returns a new locally-initiated stream. - * - * @param out true to create an output stream that we can use to send data to the remote peer. - * Corresponds to {@code FLAG_FIN}. - * @param in true to create an input stream that the remote peer can use to send data to us. - * Corresponds to {@code FLAG_UNIDIRECTIONAL}. - */ - public SpdyStream newStream(List
requestHeaders, boolean out, boolean in) - throws IOException { - return newStream(0, requestHeaders, out, in); - } - - private SpdyStream newStream(int associatedStreamId, List
requestHeaders, boolean out, - boolean in) throws IOException { - boolean outFinished = !out; - boolean inFinished = !in; - SpdyStream stream; - int streamId; - - synchronized (frameWriter) { - synchronized (this) { - if (shutdown) { - throw new IOException("shutdown"); - } - streamId = nextStreamId; - nextStreamId += 2; - stream = new SpdyStream(streamId, this, outFinished, inFinished, requestHeaders); - if (stream.isOpen()) { - streams.put(streamId, stream); - setIdle(false); - } - } - if (associatedStreamId == 0) { - frameWriter.synStream(outFinished, inFinished, streamId, associatedStreamId, - requestHeaders); - } else if (client) { - throw new IllegalArgumentException("client streams shouldn't have associated stream IDs"); - } else { // HTTP/2 has a PUSH_PROMISE frame. - frameWriter.pushPromise(associatedStreamId, streamId, requestHeaders); - } - } - - if (!out) { - frameWriter.flush(); - } - - return stream; - } - - void writeSynReply(int streamId, boolean outFinished, List
alternating) - throws IOException { - frameWriter.synReply(outFinished, streamId, alternating); - } - - /** - * Callers of this method are not thread safe, and sometimes on application - * threads. Most often, this method will be called to send a buffer worth of - * data to the peer. - *

- * Writes are subject to the write window of the stream and the connection. - * Until there is a window sufficient to send {@code byteCount}, the caller - * will block. For example, a user of {@code HttpURLConnection} who flushes - * more bytes to the output stream than the connection's write window will - * block. - *

- * Zero {@code byteCount} writes are not subject to flow control and - * will not block. The only use case for zero {@code byteCount} is closing - * a flushed output stream. - */ - public void writeData(int streamId, boolean outFinished, Buffer buffer, long byteCount) - throws IOException { - if (byteCount == 0) { // Empty data frames are not flow-controlled. - frameWriter.data(outFinished, streamId, buffer, 0); - return; - } - - while (byteCount > 0) { - int toWrite; - synchronized (SpdyConnection.this) { - try { - while (bytesLeftInWriteWindow <= 0) { - SpdyConnection.this.wait(); // Wait until we receive a WINDOW_UPDATE. - } - } catch (InterruptedException e) { - throw new InterruptedIOException(); - } - - toWrite = (int) Math.min(Math.min(byteCount, bytesLeftInWriteWindow), maxFrameSize); - bytesLeftInWriteWindow -= toWrite; - } - - byteCount -= toWrite; - frameWriter.data(outFinished && byteCount == 0, streamId, buffer, toWrite); - } - } - - /** - * {@code delta} will be negative if a settings frame initial window is - * smaller than the last. - */ - void addBytesToWriteWindow(long delta) { - bytesLeftInWriteWindow += delta; - if (delta > 0) SpdyConnection.this.notifyAll(); - } - - void writeSynResetLater(final int streamId, final ErrorCode errorCode) { - executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) { - @Override public void execute() { - try { - writeSynReset(streamId, errorCode); - } catch (IOException ignored) { - } - } - }); - } - - void writeSynReset(int streamId, ErrorCode statusCode) throws IOException { - frameWriter.rstStream(streamId, statusCode); - } - - void writeWindowUpdateLater(final int streamId, final long unacknowledgedBytesRead) { - executor.submit(new NamedRunnable("OkHttp Window Update %s stream %d", hostName, streamId) { - @Override public void execute() { - try { - frameWriter.windowUpdate(streamId, unacknowledgedBytesRead); - } catch (IOException ignored) { - } - } - }); - } - - /** - * Sends a ping frame to the peer. Use the returned object to await the - * ping's response and observe its round trip time. - */ - public Ping ping() throws IOException { - Ping ping = new Ping(); - int pingId; - synchronized (this) { - if (shutdown) { - throw new IOException("shutdown"); - } - pingId = nextPingId; - nextPingId += 2; - if (pings == null) pings = new HashMap(); - pings.put(pingId, ping); - } - writePing(false, pingId, 0x4f4b6f6b /* ASCII "OKok" */, ping); - return ping; - } - - private void writePingLater( - final boolean reply, final int payload1, final int payload2, final Ping ping) { - executor.submit(new NamedRunnable("OkHttp %s ping %08x%08x", - hostName, payload1, payload2) { - @Override public void execute() { - try { - writePing(reply, payload1, payload2, ping); - } catch (IOException ignored) { - } - } - }); - } - - 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(); - frameWriter.ping(reply, payload1, payload2); - } - } - - private synchronized Ping removePing(int id) { - return pings != null ? pings.remove(id) : null; - } - - public void flush() throws IOException { - frameWriter.flush(); - } - - /** - * Degrades this connection such that new streams can neither be created - * locally, nor accepted from the remote peer. Existing streams are not - * impacted. This is intended to permit an endpoint to gracefully stop - * accepting new requests without harming previously established streams. - */ - public void shutdown(ErrorCode statusCode) throws IOException { - synchronized (frameWriter) { - int lastGoodStreamId; - synchronized (this) { - if (shutdown) { - return; - } - shutdown = true; - lastGoodStreamId = this.lastGoodStreamId; - } - // TODO: propagate exception message into debugData - frameWriter.goAway(lastGoodStreamId, statusCode, Util.EMPTY_BYTE_ARRAY); - } - } - - /** - * Closes this connection. This cancels all open streams and unanswered - * pings. It closes the underlying input and output streams and shuts down - * internal executor services. - */ - @Override public void close() throws IOException { - close(ErrorCode.NO_ERROR, ErrorCode.CANCEL); - } - - private void close(ErrorCode connectionCode, ErrorCode streamCode) throws IOException { - assert (!Thread.holdsLock(this)); - IOException thrown = null; - try { - shutdown(connectionCode); - } catch (IOException e) { - thrown = e; - } - - SpdyStream[] streamsToClose = null; - Ping[] pingsToCancel = null; - synchronized (this) { - if (!streams.isEmpty()) { - streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]); - streams.clear(); - setIdle(false); - } - if (pings != null) { - pingsToCancel = pings.values().toArray(new Ping[pings.size()]); - pings = null; - } - } - - if (streamsToClose != null) { - for (SpdyStream stream : streamsToClose) { - try { - stream.close(streamCode); - } catch (IOException e) { - if (thrown != null) thrown = e; - } - } - } - - if (pingsToCancel != null) { - for (Ping ping : pingsToCancel) { - ping.cancel(); - } - } - - // Close the writer to release its resources (such as deflaters). - try { - frameWriter.close(); - } catch (IOException e) { - if (thrown == null) thrown = e; - } - - // Close the socket to break out the reader thread, which will clean up after itself. - try { - socket.close(); - } catch (IOException e) { - thrown = e; - } - - 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 sendConnectionPreface() throws IOException { - frameWriter.connectionPreface(); - frameWriter.settings(okHttpSettings); - int windowSize = okHttpSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); - if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) { - frameWriter.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE); - } - } - - public static class Builder { - private String hostName; - private Socket socket; - private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS; - private Protocol protocol = Protocol.SPDY_3; - private PushObserver pushObserver = PushObserver.CANCEL; - private boolean client; - - public Builder(boolean client, Socket socket) throws IOException { - this(((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(), client, socket); - } - - /** - * @param client true if this peer initiated the connection; false if this - * peer accepted the connection. - */ - public Builder(String hostName, boolean client, Socket socket) throws IOException { - this.hostName = hostName; - this.client = client; - this.socket = socket; - } - - public Builder handler(IncomingStreamHandler handler) { - this.handler = handler; - return this; - } - - public Builder protocol(Protocol protocol) { - this.protocol = protocol; - return this; - } - - public Builder pushObserver(PushObserver pushObserver) { - this.pushObserver = pushObserver; - return this; - } - - public SpdyConnection build() throws IOException { - return new SpdyConnection(this); - } - } - - /** - * Methods in this class must not lock FrameWriter. If a method needs to - * write a frame, create an async task to do so. - */ - class Reader extends NamedRunnable implements FrameReader.Handler { - FrameReader frameReader; - - private Reader() { - super("OkHttp %s", hostName); - } - - @Override protected void execute() { - ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR; - ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR; - try { - frameReader = variant.newReader(Okio.buffer(Okio.source(socket)), client); - if (!client) { - frameReader.readConnectionPreface(); - } - while (frameReader.nextFrame(this)) { - } - connectionErrorCode = ErrorCode.NO_ERROR; - streamErrorCode = ErrorCode.CANCEL; - } catch (IOException e) { - connectionErrorCode = ErrorCode.PROTOCOL_ERROR; - streamErrorCode = ErrorCode.PROTOCOL_ERROR; - } finally { - try { - close(connectionErrorCode, streamErrorCode); - } catch (IOException ignored) { - } - Util.closeQuietly(frameReader); - } - } - - @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length) - throws IOException { - if (pushedStream(streamId)) { - pushDataLater(streamId, source, length, inFinished); - return; - } - SpdyStream dataStream = getStream(streamId); - if (dataStream == null) { - writeSynResetLater(streamId, ErrorCode.INVALID_STREAM); - source.skip(length); - return; - } - dataStream.receiveData(source, length); - if (inFinished) { - dataStream.receiveFin(); - } - } - - @Override public void headers(boolean outFinished, boolean inFinished, int streamId, - int associatedStreamId, List

headerBlock, HeadersMode headersMode) { - if (pushedStream(streamId)) { - pushHeadersLater(streamId, headerBlock, inFinished); - return; - } - SpdyStream stream; - synchronized (SpdyConnection.this) { - // 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, headerBlock); - lastGoodStreamId = streamId; - streams.put(streamId, newStream); - executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) { - @Override public void execute() { - try { - handler.receive(newStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }); - return; - } - } - - // 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; - } - - // Update an existing stream. - stream.receiveHeaders(headerBlock, headersMode); - if (inFinished) stream.receiveFin(); - } - - @Override public void rstStream(int streamId, ErrorCode errorCode) { - if (pushedStream(streamId)) { - pushResetLater(streamId, errorCode); - return; - } - SpdyStream rstStream = removeStream(streamId); - if (rstStream != null) { - rstStream.receiveRstStream(errorCode); - } - } - - @Override public void settings(boolean clearPrevious, Settings newSettings) { - long delta = 0; - SpdyStream[] streamsToNotify = null; - synchronized (SpdyConnection.this) { - int priorWriteWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); - if (clearPrevious) peerSettings.clear(); - peerSettings.merge(newSettings); - if (getProtocol() == Protocol.HTTP_2) { - ackSettingsLater(); - } - int peerInitialWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); - if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) { - delta = peerInitialWindowSize - priorWriteWindowSize; - if (!receivedInitialPeerSettings) { - addBytesToWriteWindow(delta); - receivedInitialPeerSettings = true; - } - if (!streams.isEmpty()) { - streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]); - } - } - } - if (streamsToNotify != null && delta != 0) { - for (SpdyStream stream : streams.values()) { - synchronized (stream) { - stream.addBytesToWriteWindow(delta); - } - } - } - } - - private void ackSettingsLater() { - executor.submit(new NamedRunnable("OkHttp %s ACK Settings", hostName) { - @Override public void execute() { - try { - frameWriter.ackSettings(); - } catch (IOException ignored) { - } - } - }); - } - - @Override public void ackSettings() { - // TODO: If we don't get this callback after sending settings to the peer, SETTINGS_TIMEOUT. - } - - @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 lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { - if (debugData.size() > 0) { // TODO: log the debugData - } - synchronized (SpdyConnection.this) { - shutdown = true; - - // Fail all streams created after the last good stream ID. - for (Iterator> i = streams.entrySet().iterator(); - i.hasNext(); ) { - Map.Entry entry = i.next(); - int streamId = entry.getKey(); - if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) { - entry.getValue().receiveRstStream(ErrorCode.REFUSED_STREAM); - i.remove(); - } - } - } - } - - @Override public void windowUpdate(int streamId, long windowSizeIncrement) { - if (streamId == 0) { - synchronized (SpdyConnection.this) { - bytesLeftInWriteWindow += windowSizeIncrement; - SpdyConnection.this.notifyAll(); - } - } else { - SpdyStream stream = getStream(streamId); - if (stream != null) { - synchronized (stream) { - stream.addBytesToWriteWindow(windowSizeIncrement); - } - } - } - } - - @Override public void priority(int streamId, int streamDependency, int weight, - boolean exclusive) { - // TODO: honor priority. - } - - @Override - public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) { - pushRequestLater(promisedStreamId, requestHeaders); - } - - @Override public void alternateService(int streamId, String origin, ByteString protocol, - String host, int port, long maxAge) { - // TODO: register alternate service. - } - } - - /** Even, positive numbered streams are pushed streams in HTTP/2. */ - private boolean pushedStream(int streamId) { - return protocol == Protocol.HTTP_2 && streamId != 0 && (streamId & 1) == 0; - } - - // Guarded by this. - private final Set currentPushRequests = new LinkedHashSet(); - - private void pushRequestLater(final int streamId, final List
requestHeaders) { - synchronized (this) { - if (currentPushRequests.contains(streamId)) { - writeSynResetLater(streamId, ErrorCode.PROTOCOL_ERROR); - return; - } - currentPushRequests.add(streamId); - } - pushExecutor.submit(new NamedRunnable("OkHttp %s Push Request[%s]", hostName, streamId) { - @Override public void execute() { - boolean cancel = pushObserver.onRequest(streamId, requestHeaders); - try { - if (cancel) { - frameWriter.rstStream(streamId, ErrorCode.CANCEL); - synchronized (SpdyConnection.this) { - currentPushRequests.remove(streamId); - } - } - } catch (IOException ignored) { - } - } - }); - } - - private void pushHeadersLater(final int streamId, final List
requestHeaders, - final boolean inFinished) { - pushExecutor.submit(new NamedRunnable("OkHttp %s Push Headers[%s]", hostName, streamId) { - @Override public void execute() { - boolean cancel = pushObserver.onHeaders(streamId, requestHeaders, inFinished); - try { - if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL); - if (cancel || inFinished) { - synchronized (SpdyConnection.this) { - currentPushRequests.remove(streamId); - } - } - } catch (IOException ignored) { - } - } - }); - } - - /** - * Eagerly reads {@code byteCount} bytes from the source before launching a background task to - * process the data. This avoids corrupting the stream. - */ - private void pushDataLater(final int streamId, final BufferedSource source, final int byteCount, - final boolean inFinished) throws IOException { - final Buffer buffer = new Buffer(); - source.require(byteCount); // Eagerly read the frame before firing client thread. - source.read(buffer, byteCount); - if (buffer.size() != byteCount) throw new IOException(buffer.size() + " != " + byteCount); - pushExecutor.submit(new NamedRunnable("OkHttp %s Push Data[%s]", hostName, streamId) { - @Override public void execute() { - try { - boolean cancel = pushObserver.onData(streamId, buffer, byteCount, inFinished); - if (cancel) frameWriter.rstStream(streamId, ErrorCode.CANCEL); - if (cancel || inFinished) { - synchronized (SpdyConnection.this) { - currentPushRequests.remove(streamId); - } - } - } catch (IOException ignored) { - } - } - }); - } - - private void pushResetLater(final int streamId, final ErrorCode errorCode) { - pushExecutor.submit(new NamedRunnable("OkHttp %s Push Reset[%s]", hostName, streamId) { - @Override public void execute() { - pushObserver.onReset(streamId, errorCode); - synchronized (SpdyConnection.this) { - currentPushRequests.remove(streamId); - } - } - }); - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyStream.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyStream.java deleted file mode 100644 index db1a487f6..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/SpdyStream.java +++ /dev/null @@ -1,577 +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.koushikdutta.async.http.spdy.okhttp.internal.spdy; - -import com.koushikdutta.async.http.spdy.okio.AsyncTimeout; -import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; -import com.koushikdutta.async.http.spdy.okio.Sink; -import com.koushikdutta.async.http.spdy.okio.Source; -import com.koushikdutta.async.http.spdy.okio.Timeout; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.util.ArrayList; -import java.util.List; - -import static com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE; - -/** A logical bidirectional stream. */ -public final class SpdyStream { - // Internal state is guarded by this. No long-running or potentially - // blocking operations are performed while the lock is held. - - /** - * The total number of bytes consumed by the application (with {@link - * SpdyDataSource#read}), but not yet acknowledged by sending a {@code - * WINDOW_UPDATE} frame on this stream. - */ - // Visible for testing - long unacknowledgedBytesRead = 0; - - /** - * Count of bytes that can be written on the stream before receiving a - * window update. Even if this is positive, writes will block until there - * available bytes in {@code connection.bytesLeftInWriteWindow}. - */ - // guarded by this - long bytesLeftInWriteWindow; - - private final int id; - private final SpdyConnection connection; - private long readTimeoutMillis = 0; - - /** Headers sent by the stream initiator. Immutable and non null. */ - private final List
requestHeaders; - - /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */ - private List
responseHeaders; - - private final SpdyDataSource source; - final SpdyDataSink sink; - private final SpdyTimeout readTimeout = new SpdyTimeout(); - private final SpdyTimeout writeTimeout = new SpdyTimeout(); - - /** - * The reason why this stream was abnormally closed. If there are multiple - * reasons to abnormally close this stream (such as both peers closing it - * near-simultaneously) then this is the first reason known to this peer. - */ - private ErrorCode errorCode = null; - - SpdyStream(int id, SpdyConnection connection, boolean outFinished, boolean inFinished, - List
requestHeaders) { - if (connection == null) throw new NullPointerException("connection == null"); - if (requestHeaders == null) throw new NullPointerException("requestHeaders == null"); - this.id = id; - this.connection = connection; - this.bytesLeftInWriteWindow = - connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE); - this.source = new SpdyDataSource( - connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE)); - this.sink = new SpdyDataSink(); - this.source.finished = inFinished; - this.sink.finished = outFinished; - this.requestHeaders = requestHeaders; - } - - public int getId() { - return id; - } - - /** - * Returns true if this stream is open. A stream is open until either: - *
    - *
  • A {@code SYN_RESET} frame abnormally terminates the stream. - *
  • Both input and output streams have transmitted all data and - * headers. - *
- * Note that the input stream may continue to yield data even after a stream - * reports itself as not open. This is because input data is buffered. - */ - public synchronized boolean isOpen() { - if (errorCode != null) { - return false; - } - if ((source.finished || source.closed) - && (sink.finished || sink.closed) - && responseHeaders != null) { - return false; - } - return true; - } - - /** Returns true if this stream was created by this peer. */ - public boolean isLocallyInitiated() { - boolean streamIsClient = ((id & 1) == 1); - return connection.client == streamIsClient; - } - - public SpdyConnection getConnection() { - return connection; - } - - public List
getRequestHeaders() { - return requestHeaders; - } - - /** - * Returns the stream's response headers, blocking if necessary if they - * have not been received yet. - */ - public synchronized List
getResponseHeaders() throws IOException { - readTimeout.enter(); - try { - while (responseHeaders == null && errorCode == null) { - waitForIo(); - } - } finally { - readTimeout.exitAndThrowIfTimedOut(); - } - if (responseHeaders != null) return responseHeaders; - throw new IOException("stream was reset: " + errorCode); - } - - /** - * Returns the reason why this stream was closed, or null if it closed - * normally or has not yet been closed. - */ - public synchronized ErrorCode getErrorCode() { - return errorCode; - } - - /** - * Sends a reply to an incoming stream. - * - * @param out true to create an output stream that we can use to send data - * to the remote peer. Corresponds to {@code FLAG_FIN}. - */ - public void reply(List
responseHeaders, boolean out) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - boolean outFinished = false; - synchronized (this) { - if (responseHeaders == null) { - throw new NullPointerException("responseHeaders == null"); - } - if (this.responseHeaders != null) { - throw new IllegalStateException("reply already sent"); - } - this.responseHeaders = responseHeaders; - if (!out) { - this.sink.finished = true; - outFinished = true; - } - } - connection.writeSynReply(id, outFinished, responseHeaders); - - if (outFinished) { - connection.flush(); - } - } - - public Timeout readTimeout() { - return readTimeout; - } - - public Timeout writeTimeout() { - return writeTimeout; - } - - /** Returns a source that reads data from the peer. */ - public Source getSource() { - return source; - } - - /** - * Returns a sink that can be used to write data to the peer. - * - * @throws IllegalStateException if this stream was initiated by the peer - * and a {@link #reply} has not yet been sent. - */ - public Sink getSink() { - synchronized (this) { - if (responseHeaders == null && !isLocallyInitiated()) { - throw new IllegalStateException("reply before requesting the sink"); - } - } - return sink; - } - - /** - * Abnormally terminate this stream. This blocks until the {@code RST_STREAM} - * frame has been transmitted. - */ - public void close(ErrorCode rstStatusCode) throws IOException { - if (!closeInternal(rstStatusCode)) { - return; // Already closed. - } - connection.writeSynReset(id, rstStatusCode); - } - - /** - * Abnormally terminate this stream. This enqueues a {@code RST_STREAM} - * frame and returns immediately. - */ - public void closeLater(ErrorCode errorCode) { - if (!closeInternal(errorCode)) { - return; // Already closed. - } - connection.writeSynResetLater(id, errorCode); - } - - /** Returns true if this stream was closed. */ - private boolean closeInternal(ErrorCode errorCode) { - assert (!Thread.holdsLock(this)); - synchronized (this) { - if (this.errorCode != null) { - return false; - } - if (source.finished && sink.finished) { - return false; - } - this.errorCode = errorCode; - notifyAll(); - } - connection.removeStream(id); - return true; - } - - void receiveHeaders(List
headers, HeadersMode headersMode) { - assert (!Thread.holdsLock(SpdyStream.this)); - ErrorCode errorCode = null; - boolean open = true; - synchronized (this) { - if (responseHeaders == null) { - if (headersMode.failIfHeadersAbsent()) { - errorCode = ErrorCode.PROTOCOL_ERROR; - } else { - responseHeaders = headers; - open = isOpen(); - notifyAll(); - } - } else { - if (headersMode.failIfHeadersPresent()) { - errorCode = ErrorCode.STREAM_IN_USE; - } else { - List
newHeaders = new ArrayList
(); - newHeaders.addAll(responseHeaders); - newHeaders.addAll(headers); - this.responseHeaders = newHeaders; - } - } - } - if (errorCode != null) { - closeLater(errorCode); - } else if (!open) { - connection.removeStream(id); - } - } - - void receiveData(BufferedSource in, int length) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - this.source.receive(in, length); - } - - void receiveFin() { - assert (!Thread.holdsLock(SpdyStream.this)); - boolean open; - synchronized (this) { - this.source.finished = true; - open = isOpen(); - notifyAll(); - } - if (!open) { - connection.removeStream(id); - } - } - - synchronized void receiveRstStream(ErrorCode errorCode) { - if (this.errorCode == null) { - this.errorCode = errorCode; - notifyAll(); - } - } - - /** - * A source that reads the incoming data frames of a stream. Although this - * class uses synchronization to safely receive incoming data frames, it is - * not intended for use by multiple readers. - */ - private final class SpdyDataSource implements Source { - /** Buffer to receive data from the network into. Only accessed by the reader thread. */ - private final Buffer receiveBuffer = new Buffer(); - - /** Buffer with readable data. Guarded by SpdyStream.this. */ - private final Buffer readBuffer = new Buffer(); - - /** Maximum number of bytes to buffer before reporting a flow control error. */ - private final long maxByteCount; - - /** True if the caller has closed this stream. */ - private boolean closed; - - /** - * True if either side has cleanly shut down this stream. We will - * receive no more bytes beyond those already in the buffer. - */ - private boolean finished; - - private SpdyDataSource(long maxByteCount) { - this.maxByteCount = maxByteCount; - } - - @Override public long read(Buffer sink, long byteCount) - throws IOException { - if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); - - long read; - synchronized (SpdyStream.this) { - waitUntilReadable(); - checkNotClosed(); - if (readBuffer.size() == 0) return -1; // This source is exhausted. - - // Move bytes from the read buffer into the caller's buffer. - read = readBuffer.read(sink, Math.min(byteCount, readBuffer.size())); - - // Flow control: notify the peer that we're ready for more data! - unacknowledgedBytesRead += read; - if (unacknowledgedBytesRead - >= connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) { - connection.writeWindowUpdateLater(id, unacknowledgedBytesRead); - unacknowledgedBytesRead = 0; - } - } - - // Update connection.unacknowledgedBytesRead outside the stream lock. - synchronized (connection) { // Multiple application threads may hit this section. - connection.unacknowledgedBytesRead += read; - if (connection.unacknowledgedBytesRead - >= connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) { - connection.writeWindowUpdateLater(0, connection.unacknowledgedBytesRead); - connection.unacknowledgedBytesRead = 0; - } - } - - return read; - } - - /** Returns once the source is either readable or finished. */ - private void waitUntilReadable() throws IOException { - readTimeout.enter(); - try { - while (readBuffer.size() == 0 && !finished && !closed && errorCode == null) { - waitForIo(); - } - } finally { - readTimeout.exitAndThrowIfTimedOut(); - } - } - - void receive(BufferedSource in, long byteCount) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - - while (byteCount > 0) { - boolean finished; - boolean flowControlError; - synchronized (SpdyStream.this) { - finished = this.finished; - flowControlError = byteCount + readBuffer.size() > maxByteCount; - } - - // If the peer sends more data than we can handle, discard it and close the connection. - if (flowControlError) { - in.skip(byteCount); - closeLater(ErrorCode.FLOW_CONTROL_ERROR); - return; - } - - // Discard data received after the stream is finished. It's probably a benign race. - if (finished) { - in.skip(byteCount); - return; - } - - // Fill the receive buffer without holding any locks. - long read = in.read(receiveBuffer, byteCount); - if (read == -1) throw new EOFException(); - byteCount -= read; - - // Move the received data to the read buffer to the reader can read it. - synchronized (SpdyStream.this) { - boolean wasEmpty = readBuffer.size() == 0; - readBuffer.writeAll(receiveBuffer); - if (wasEmpty) { - SpdyStream.this.notifyAll(); - } - } - } - } - - @Override public Timeout timeout() { - return readTimeout; - } - - @Override public void close() throws IOException { - synchronized (SpdyStream.this) { - closed = true; - readBuffer.clear(); - SpdyStream.this.notifyAll(); - } - cancelStreamIfNecessary(); - } - - private void checkNotClosed() throws IOException { - if (closed) { - throw new IOException("stream closed"); - } - if (errorCode != null) { - throw new IOException("stream was reset: " + errorCode); - } - } - } - - private void cancelStreamIfNecessary() throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - boolean open; - boolean cancel; - synchronized (this) { - cancel = !source.finished && source.closed && (sink.finished || sink.closed); - open = isOpen(); - } - if (cancel) { - // RST this stream to prevent additional data from being sent. This - // is safe because the input stream is closed (we won't use any - // further bytes) and the output stream is either finished or closed - // (so RSTing both streams doesn't cause harm). - SpdyStream.this.close(ErrorCode.CANCEL); - } else if (!open) { - connection.removeStream(id); - } - } - - /** - * A sink that writes outgoing data frames of a stream. This class is not - * thread safe. - */ - final class SpdyDataSink implements Sink { - private boolean closed; - - /** - * True if either side has cleanly shut down this stream. We shall send - * no more bytes. - */ - private boolean finished; - - @Override public void write(Buffer source, long byteCount) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - while (byteCount > 0) { - long toWrite; - synchronized (SpdyStream.this) { - writeTimeout.enter(); - try { - while (bytesLeftInWriteWindow <= 0 && !finished && !closed && errorCode == null) { - waitForIo(); // Wait until we receive a WINDOW_UPDATE. - } - } finally { - writeTimeout.exitAndThrowIfTimedOut(); - } - - checkOutNotClosed(); // Kick out if the stream was reset or closed while waiting. - toWrite = Math.min(bytesLeftInWriteWindow, byteCount); - bytesLeftInWriteWindow -= toWrite; - } - - byteCount -= toWrite; - connection.writeData(id, false, source, toWrite); - } - } - - @Override public void flush() throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - synchronized (SpdyStream.this) { - checkOutNotClosed(); - } - connection.flush(); - } - - @Override public Timeout timeout() { - return writeTimeout; - } - - @Override public void close() throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - synchronized (SpdyStream.this) { - if (closed) return; - } - if (!sink.finished) { - connection.writeData(id, true, null, 0); - } - synchronized (SpdyStream.this) { - closed = true; - } - connection.flush(); - cancelStreamIfNecessary(); - } - } - - /** - * {@code delta} will be negative if a settings frame initial window is - * smaller than the last. - */ - void addBytesToWriteWindow(long delta) { - bytesLeftInWriteWindow += delta; - if (delta > 0) SpdyStream.this.notifyAll(); - } - - private void checkOutNotClosed() throws IOException { - if (sink.closed) { - throw new IOException("stream closed"); - } else if (sink.finished) { - throw new IOException("stream finished"); - } else if (errorCode != null) { - throw new IOException("stream was reset: " + errorCode); - } - } - - /** - * Like {@link #wait}, but throws an {@code InterruptedIOException} when - * interrupted instead of the more awkward {@link InterruptedException}. - */ - private void waitForIo() throws InterruptedIOException { - try { - wait(); - } catch (InterruptedException e) { - throw new InterruptedIOException(); - } - } - - /** - * The Okio timeout watchdog will call {@link #timedOut} if the timeout is - * reached. In that case we close the stream (asynchronously) which will - * notify the waiting thread. - */ - class SpdyTimeout extends AsyncTimeout { - @Override protected void timedOut() { - closeLater(ErrorCode.CANCEL); - } - - public void exitAndThrowIfTimedOut() throws InterruptedIOException { - if (exit()) throw new InterruptedIOException("timeout"); - } - } -} From 432c80258b0d164f291cbb30a08d994b11f77ce4 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sun, 27 Jul 2014 19:53:50 -0700 Subject: [PATCH 050/399] refactor w/out framereader okio --- .../com/koushikdutta/async/PushParser.java | 3 +- .../async/http/spdy/AsyncSpdyConnection.java | 60 +- .../okhttp/internal/spdy/FrameReader.java | 223 ++--- .../okhttp/internal/spdy/HeaderReader.java | 69 ++ ...Draft13.java => Http20Draft13.java.ignore} | 1 + .../internal/spdy/NameValueBlockReader.java | 119 --- .../http/spdy/okhttp/internal/spdy/Spdy3.java | 918 +++++++++--------- .../spdy/okhttp/internal/spdy/Variant.java | 4 +- 8 files changed, 697 insertions(+), 700 deletions(-) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java rename AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/{Http20Draft13.java => Http20Draft13.java.ignore} (99%) delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/NameValueBlockReader.java diff --git a/AndroidAsync/src/com/koushikdutta/async/PushParser.java b/AndroidAsync/src/com/koushikdutta/async/PushParser.java index e02ac7f93..51a79cc3f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/PushParser.java +++ b/AndroidAsync/src/com/koushikdutta/async/PushParser.java @@ -237,8 +237,9 @@ public void parsed(byte[] data) { private ArrayList args = new ArrayList(); ByteOrder order = ByteOrder.BIG_ENDIAN; - public void setOrder(ByteOrder order) { + public PushParser setOrder(ByteOrder order) { this.order = order; + return this; } public PushParser(DataEmitter s) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index 7471615ad..d9feaf708 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -5,31 +5,25 @@ import com.koushikdutta.async.BufferedDataEmitter; import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.future.SimpleFuture; -import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.ErrorCode; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameReader; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameWriter; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.HeadersMode; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Http20Draft13; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Ping; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Variant; import com.koushikdutta.async.http.spdy.okio.BufferedSink; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; import com.koushikdutta.async.http.spdy.okio.ByteString; import com.koushikdutta.async.http.spdy.okio.Okio; -import junit.framework.Assert; - import java.io.IOException; import java.util.Hashtable; import java.util.Iterator; @@ -42,21 +36,17 @@ * Created by koush on 7/16/14. */ public class AsyncSpdyConnection implements FrameReader.Handler { - BufferedDataEmitter emitter; AsyncSocket socket; BufferedDataSink bufferedSocket; FrameReader reader; FrameWriter writer; Variant variant; -// SpdySocket zero = new SpdySocket(0, false, false, null); - ByteBufferListSource source = new ByteBufferListSource(); ByteBufferListSink sink = new ByteBufferListSink() { @Override public void flush() throws IOException { AsyncSpdyConnection.this.flush(); } }; - BufferedSource bufferedSource; BufferedSink bufferedSink; Hashtable sockets = new Hashtable(); Protocol protocol; @@ -286,16 +276,15 @@ public AsyncSpdyConnection(AsyncSocket socket, Protocol protocol) { this.protocol = protocol; this.socket = socket; this.bufferedSocket = new BufferedDataSink(socket); - emitter = new BufferedDataEmitter(socket); - emitter.setDataCallback(callback); if (protocol == Protocol.SPDY_3) { variant = new Spdy3(); } else if (protocol == Protocol.HTTP_2) { - variant = new Http20Draft13(); + throw new AssertionError("http20draft13"); +// variant = new Http20Draft13(); } - reader = variant.newReader(bufferedSource = Okio.buffer(source), true); + reader = variant.newReader(socket, this, true); writer = variant.newWriter(bufferedSink = Okio.buffer(sink), true); boolean client = true; @@ -313,22 +302,6 @@ else if (protocol == Protocol.HTTP_2) { } } - DataCallback callback = new DataCallback() { - @Override - public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { - int needed; - while ((needed = reader.canProcessFrame(bb)) > 0) { - bb.get(source, needed); - try { - reader.nextFrame(AsyncSpdyConnection.this); - } - catch (IOException e) { - throw new AssertionError(e); - } - } - } - }; - /** * Sends a connection header if the current variant requires it. This should * be called after {@link Builder#build} for all new connections. @@ -348,7 +321,7 @@ private boolean pushedStream(int streamId) { } @Override - public void data(boolean inFinished, int streamId, BufferedSource source, int length) throws IOException { + public void data(boolean inFinished, int streamId, ByteBufferList source) { if (pushedStream(streamId)) { throw new AssertionError("push"); // pushDataLater(streamId, source, length, inFinished); @@ -356,14 +329,17 @@ public void data(boolean inFinished, int streamId, BufferedSource source, int le } SpdySocket socket = sockets.get(streamId); if (socket == null) { - writer.rstStream(streamId, ErrorCode.INVALID_STREAM); - source.skip(length); + try { + writer.rstStream(streamId, ErrorCode.INVALID_STREAM); + } + catch (IOException e) { + throw new AssertionError(e); + } + source.recycle(); return; } - if (source != this.bufferedSource || this.source.remaining() + source.buffer().size() != length) - throw new AssertionError(); - source.buffer().readAll(socket.pending); - this.source.get(socket.pending); + int length = source.remaining(); + source.get(socket.pending); socket.updateWindowRead(length); Util.emitAllData(socket, socket.pending); if (inFinished) { @@ -562,4 +538,14 @@ public void pushPromise(int streamId, int promisedStreamId, List
request @Override public void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge) { } + + @Override + public void error(Exception e) { + socket.close(); + for (Iterator> i = sockets.entrySet().iterator(); i.hasNext();) { + Map.Entry entry = i.next(); + Util.end(entry.getValue(), e); + i.remove(); + } + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java index 19b6b77a0..3f457fb0c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java @@ -17,124 +17,129 @@ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; import com.koushikdutta.async.http.spdy.okio.ByteString; import java.io.Closeable; import java.io.IOException; import java.util.List; -/** Reads transport frames for SPDY/3 or HTTP/2. */ +/** + * Reads transport frames for SPDY/3 or HTTP/2. + */ public interface FrameReader extends Closeable { - int canProcessFrame(ByteBufferList bb); - void readConnectionPreface() throws IOException; - boolean nextFrame(Handler handler) throws IOException; + void readConnectionPreface() throws IOException; +// boolean nextFrame(Handler handler) throws IOException; - public interface Handler { - void data(boolean inFinished, int streamId, BufferedSource source, int length) - throws IOException; + public interface Handler { + void error(Exception e); + + void data(boolean inFinished, int streamId, ByteBufferList bb); + + /** + * 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 HEADERS and PUSH_PROMISE. + * + * @param outFinished true if the receiver should not send further frames. + * @param inFinished true if the sender will not send further frames. + * @param streamId the stream owning these headers. + * @param associatedStreamId the stream that triggered the sender to create + * this stream. + */ + void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, + List
headerBlock, HeadersMode headersMode); + + void rstStream(int streamId, ErrorCode errorCode); + + void settings(boolean clearPrevious, Settings settings); + + /** + * HTTP/2 only. + */ + void ackSettings(); + + /** + * Read a connection-level ping from the peer. {@code ack} indicates this + * is a reply. Payload parameters are different between SPDY/3 and HTTP/2. + *

+ * In SPDY/3, only the first {@code payload1} parameter is set. If the + * reader is a client, it is an unsigned even number. Likewise, a server + * will receive an odd number. + *

+ * In HTTP/2, both {@code payload1} and {@code payload2} parameters are + * set. The data is opaque binary, and there are no rules on the content. + */ + void ping(boolean ack, int payload1, int payload2); + + /** + * The peer tells us to stop creating streams. It is safe to replay + * streams with {@code ID > lastGoodStreamId} on a new connection. In- + * flight streams with {@code ID <= lastGoodStreamId} can only be replayed + * on a new connection if they are idempotent. + * + * @param lastGoodStreamId the last stream ID the peer processed before + * sending this message. If {@code lastGoodStreamId} is zero, the peer + * processed no frames. + * @param errorCode reason for closing the connection. + * @param debugData only valid for HTTP/2; opaque debug data to send. + */ + void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData); + + /** + * Notifies that an additional {@code windowSizeIncrement} bytes can be + * sent on {@code streamId}, or the connection if {@code streamId} is zero. + */ + void windowUpdate(int streamId, long windowSizeIncrement); + + /** + * Called when reading a headers or priority frame. This may be used to + * change the stream's weight from the default (16) to a new value. + * + * @param streamId stream which has a priority change. + * @param streamDependency the stream ID this stream is dependent on. + * @param weight relative proportion of priority in [1..256]. + * @param exclusive inserts this stream ID as the sole child of + * {@code streamDependency}. + */ + void priority(int streamId, int streamDependency, int weight, boolean exclusive); - /** - * 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 HEADERS and PUSH_PROMISE. - * - * @param outFinished true if the receiver should not send further frames. - * @param inFinished true if the sender will not send further frames. - * @param streamId the stream owning these headers. - * @param associatedStreamId the stream that triggered the sender to create - * this stream. - */ - void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, - List

headerBlock, HeadersMode headersMode); - void rstStream(int streamId, ErrorCode errorCode); - void settings(boolean clearPrevious, Settings settings); - - /** HTTP/2 only. */ - void ackSettings(); - - /** - * Read a connection-level ping from the peer. {@code ack} indicates this - * is a reply. Payload parameters are different between SPDY/3 and HTTP/2. - *

- * In SPDY/3, only the first {@code payload1} parameter is set. If the - * reader is a client, it is an unsigned even number. Likewise, a server - * will receive an odd number. - *

- * In HTTP/2, both {@code payload1} and {@code payload2} parameters are - * set. The data is opaque binary, and there are no rules on the content. - */ - void ping(boolean ack, int payload1, int payload2); - - /** - * The peer tells us to stop creating streams. It is safe to replay - * streams with {@code ID > lastGoodStreamId} on a new connection. In- - * flight streams with {@code ID <= lastGoodStreamId} can only be replayed - * on a new connection if they are idempotent. - * - * @param lastGoodStreamId the last stream ID the peer processed before - * sending this message. If {@code lastGoodStreamId} is zero, the peer - * processed no frames. - * @param errorCode reason for closing the connection. - * @param debugData only valid for HTTP/2; opaque debug data to send. - */ - void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData); - - /** - * Notifies that an additional {@code windowSizeIncrement} bytes can be - * sent on {@code streamId}, or the connection if {@code streamId} is zero. - */ - void windowUpdate(int streamId, long windowSizeIncrement); - - /** - * Called when reading a headers or priority frame. This may be used to - * change the stream's weight from the default (16) to a new value. - * - * @param streamId stream which has a priority change. - * @param streamDependency the stream ID this stream is dependent on. - * @param weight relative proportion of priority in [1..256]. - * @param exclusive inserts this stream ID as the sole child of - * {@code streamDependency}. - */ - void priority(int streamId, int streamDependency, int weight, boolean exclusive); - - /** - * HTTP/2 only. Receive a push promise header block. - *

- * A push promise contains all the headers that pertain to a server-initiated - * request, and a {@code promisedStreamId} to which response frames will be - * delivered. Push promise frames are sent as a part of the response to - * {@code streamId}. - * - * @param streamId client-initiated stream ID. Must be an odd number. - * @param promisedStreamId server-initiated stream ID. Must be an even - * number. - * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, - * {@code :authority}, and (@code :path}. - */ - void pushPromise(int streamId, int promisedStreamId, List

requestHeaders) + /** + * HTTP/2 only. Receive a push promise header block. + *

+ * A push promise contains all the headers that pertain to a server-initiated + * request, and a {@code promisedStreamId} to which response frames will be + * delivered. Push promise frames are sent as a part of the response to + * {@code streamId}. + * + * @param streamId client-initiated stream ID. Must be an odd number. + * @param promisedStreamId server-initiated stream ID. Must be an even + * number. + * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, + * {@code :authority}, and (@code :path}. + */ + void pushPromise(int streamId, int promisedStreamId, List

requestHeaders) throws IOException; - /** - * HTTP/2 only. Expresses that resources for the connection or a client- - * initiated stream are available from a different network location or - * protocol configuration. - * - *

See alt-svc - * - * @param streamId when a client-initiated stream ID (odd number), the - * origin of this alternate service is the origin of the stream. When - * zero, the origin is specified in the {@code origin} parameter. - * @param origin when present, the - * origin is typically - * represented as a combination of scheme, host and port. When empty, - * the origin is that of the {@code streamId}. - * @param protocol an ALPN protocol, such as {@code h2}. - * @param host an IP address or hostname. - * @param port the IP port associated with the service. - * @param maxAge time in seconds that this alternative is considered fresh. - */ - void alternateService(int streamId, String origin, ByteString protocol, String host, int port, - long maxAge); - } + /** + * HTTP/2 only. Expresses that resources for the connection or a client- + * initiated stream are available from a different network location or + * protocol configuration. + *

+ *

See alt-svc + * + * @param streamId when a client-initiated stream ID (odd number), the + * origin of this alternate service is the origin of the stream. When + * zero, the origin is specified in the {@code origin} parameter. + * @param origin when present, the + * origin is typically + * represented as a combination of scheme, host and port. When empty, + * the origin is that of the {@code streamId}. + * @param protocol an ALPN protocol, such as {@code h2}. + * @param host an IP address or hostname. + * @param port the IP port associated with the service. + * @param maxAge time in seconds that this alternative is considered fresh. + */ + void alternateService(int streamId, String origin, ByteString protocol, String host, int port, + long maxAge); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java new file mode 100644 index 000000000..618f7053b --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java @@ -0,0 +1,69 @@ +package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; + +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.http.spdy.okio.ByteString; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * Created by koush on 7/27/14. + */ +public class HeaderReader { + Inflater inflater; + public HeaderReader() { + 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; + } + }; + } + + public List

readHeader(ByteBufferList bb, int length) throws IOException { + byte[] bytes = new byte[length]; + bb.get(bytes); + + inflater.setInput(bytes); + + ByteBufferList source = new ByteBufferList().order(ByteOrder.BIG_ENDIAN); + while (!inflater.needsInput()) { + ByteBuffer b = ByteBufferList.obtain(8192); + try { + int read = inflater.inflate(b.array()); + b.limit(read); + source.add(b); + } + catch (DataFormatException e) { + throw new IOException(e); + } + } + + int numberOfPairs = source.getInt(); + List
entries = new ArrayList
(numberOfPairs); + for (int i = 0; i < numberOfPairs; i++) { + ByteString name = readByteString(source).toAsciiLowercase(); + ByteString values = readByteString(source); + if (name.size() == 0) throw new IOException("name.size == 0"); + entries.add(new Header(name, values)); + } + return entries; + } + + private static ByteString readByteString(ByteBufferList source) { + int length = source.getInt(); + byte[] bytes = new byte[length]; + source.get(bytes); + return ByteString.of(bytes); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java.ignore similarity index 99% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java.ignore index 1425cbb5d..c2231970a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java.ignore @@ -38,6 +38,7 @@ * Read and write HTTP/2 v13 frames. *

http://tools.ietf.org/html/draft-ietf-httpbis-http2-13 */ + public final class Http20Draft13 implements Variant { private static final Logger logger = Logger.getLogger(Http20Draft13.class.getName()); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/NameValueBlockReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/NameValueBlockReader.java deleted file mode 100644 index adc15f898..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/NameValueBlockReader.java +++ /dev/null @@ -1,119 +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.koushikdutta.async.http.spdy.okhttp.internal.spdy; - -import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; -import com.koushikdutta.async.http.spdy.okio.ByteString; -import com.koushikdutta.async.http.spdy.okio.ForwardingSource; -import com.koushikdutta.async.http.spdy.okio.InflaterSource; -import com.koushikdutta.async.http.spdy.okio.Okio; -import com.koushikdutta.async.http.spdy.okio.Source; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.DataFormatException; -import java.util.zip.Inflater; - -/** - * 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 { - /** This source transforms compressed bytes into uncompressed bytes. */ - private final InflaterSource inflaterSource; - - /** - * How many compressed bytes must be read into inflaterSource before - * {@link #readNameValueBlock} returns. - */ - private int compressedLimit; - - /** This source holds inflated bytes. */ - private final BufferedSource source; - - public NameValueBlockReader(BufferedSource source) { - // 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. - Source throttleSource = new ForwardingSource(source) { - @Override public long read(Buffer sink, long byteCount) throws IOException { - if (compressedLimit == 0) return -1; // Out of data for the current block. - long read = super.read(sink, Math.min(byteCount, compressedLimit)); - if (read == -1) return -1; - compressedLimit -= read; - return read; - } - }; - - // 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; - } - }; - - this.inflaterSource = new InflaterSource(throttleSource, inflater); - this.source = Okio.buffer(inflaterSource); - } - - public List

readNameValueBlock(int length) throws IOException { - this.compressedLimit += length; - - int numberOfPairs = source.readInt(); - if (numberOfPairs < 0) throw new IOException("numberOfPairs < 0: " + numberOfPairs); - if (numberOfPairs > 1024) throw new IOException("numberOfPairs > 1024: " + numberOfPairs); - - List
entries = new ArrayList
(numberOfPairs); - for (int i = 0; i < numberOfPairs; i++) { - ByteString name = readByteString().toAsciiLowercase(); - ByteString values = readByteString(); - if (name.size() == 0) throw new IOException("name.size == 0"); - entries.add(new Header(name, values)); - } - - doneReading(); - return entries; - } - - private ByteString readByteString() throws IOException { - int length = source.readInt(); - return source.readByteString(length); - } - - private void doneReading() throws IOException { - // Move any outstanding unread bytes into the inflater. One side-effect of - // deflate compression is that sometimes there are bytes remaining in the - // stream after we've consumed all of the content. - if (compressedLimit > 0) { - inflaterSource.refill(); - if (compressedLimit != 0) throw new IOException("compressedLimit > 0: " + compressedLimit); - } - } - - public void close() throws IOException { - source.close(); - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java index e38f3f01b..2868ff225 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java @@ -16,11 +16,13 @@ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.DataEmitterReader; +import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.spdy.okhttp.internal.Util; import com.koushikdutta.async.http.spdy.okio.Buffer; import com.koushikdutta.async.http.spdy.okio.BufferedSink; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; import com.koushikdutta.async.http.spdy.okio.ByteString; import com.koushikdutta.async.http.spdy.okio.DeflaterSink; import com.koushikdutta.async.http.spdy.okio.Okio; @@ -29,7 +31,6 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.ProtocolException; -import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.List; import java.util.zip.Deflater; @@ -41,477 +42,530 @@ */ public final class Spdy3 implements Variant { - @Override public Protocol getProtocol() { - return Protocol.SPDY_3; - } - - 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_PING = 0x6; - static final int TYPE_GOAWAY = 0x7; - static final int TYPE_HEADERS = 0x8; - static final int TYPE_WINDOW_UPDATE = 0x9; - - 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(Charsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(); - } - } - - @Override public FrameReader newReader(BufferedSource source, boolean client) { - return new Reader(source, client); - } - - @Override public FrameWriter newWriter(BufferedSink sink, boolean client) { - return new Writer(sink, client); - } - - @Override public int maxFrameSize() { - return 16383; - } - - /** Read spdy/3 frames. */ - static final class Reader implements FrameReader { - private final BufferedSource source; - private final boolean client; - private final NameValueBlockReader headerBlockReader; - - Reader(BufferedSource source, boolean client) { - this.source = source; - this.headerBlockReader = new NameValueBlockReader(this.source); - this.client = client; + @Override + public Protocol getProtocol() { + return Protocol.SPDY_3; + } + + 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_PING = 0x6; + static final int TYPE_GOAWAY = 0x7; + static final int TYPE_HEADERS = 0x8; + static final int TYPE_WINDOW_UPDATE = 0x9; + + 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(Charsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } } - @Override public void readConnectionPreface() { + @Override + public FrameReader newReader(DataEmitter source, FrameReader.Handler handler, boolean client) { + return new Reader(source, handler, client); } - @Override - public int canProcessFrame(ByteBufferList bb) { - if (source.buffer().size() + bb.remaining() < 8) - return 0; - ByteBuffer peek = ByteBuffer.wrap(bb.peekBytes(8)).order(ByteOrder.BIG_ENDIAN); - int w1 = peek.getInt(); - int w2 = peek.getInt(); - - int length = (w2 & 0xffffff); - if (bb.remaining() < 8 + length) - return 0; - return 8 + length; - } - - /** - * 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; - int w2; - try { - w1 = source.readInt(); - w2 = source.readInt(); - } catch (IOException e) { - return false; // This might be a normal socket close. - } - - 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_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; - - default: - source.skip(length); - return true; - } - } else { - int streamId = w1 & 0x7fffffff; - boolean inFinished = (flags & FLAG_FIN) != 0; - handler.data(inFinished, streamId, source, length); - return true; - } + @Override + public FrameWriter newWriter(BufferedSink sink, boolean client) { + return new Writer(sink, client); } - private void readSynStream(Handler handler, int flags, int length) throws IOException { - int w1 = source.readInt(); - int w2 = source.readInt(); - int streamId = w1 & 0x7fffffff; - int associatedStreamId = w2 & 0x7fffffff; - source.readShort(); // int priority = (s3 & 0xe000) >>> 13; int slot = s3 & 0xff; - List
headerBlock = headerBlockReader.readNameValueBlock(length - 10); - - boolean inFinished = (flags & FLAG_FIN) != 0; - boolean outFinished = (flags & FLAG_UNIDIRECTIONAL) != 0; - handler.headers(outFinished, inFinished, streamId, associatedStreamId, headerBlock, - HeadersMode.SPDY_SYN_STREAM); + @Override + public int maxFrameSize() { + return 16383; } - private void readSynReply(Handler handler, int flags, int length) throws IOException { - int w1 = source.readInt(); - int streamId = w1 & 0x7fffffff; - List
headerBlock = headerBlockReader.readNameValueBlock(length - 4); - boolean inFinished = (flags & FLAG_FIN) != 0; - handler.headers(false, inFinished, streamId, -1, headerBlock, HeadersMode.SPDY_REPLY); - } + /** + * Read spdy/3 frames. + */ + static final class Reader implements FrameReader { + private final HeaderReader headerReader = new HeaderReader(); + private final DataEmitter emitter; + private final boolean client; + private final Handler handler; + private final DataEmitterReader reader; + + Reader(DataEmitter emitter, Handler handler, boolean client) { + this.emitter = emitter; + this.handler = handler; + this.client = client; + + reader = new DataEmitterReader(); + parseFrameHeader(); + } - 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 = source.readInt() & 0x7fffffff; - int errorCodeInt = source.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 parseFrameHeader() { + emitter.setDataCallback(reader); + reader.read(8, onFrame); + } - private void readHeaders(Handler handler, int flags, int length) throws IOException { - int w1 = source.readInt(); - int streamId = w1 & 0x7fffffff; - List
headerBlock = headerBlockReader.readNameValueBlock(length - 4); - handler.headers(false, false, streamId, -1, headerBlock, HeadersMode.SPDY_HEADERS); - } + int w1; + int w2; + int flags; + int length; + int streamId; + boolean inFinished; + private final DataCallback onFrame = new DataCallback() { + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + bb.order(ByteOrder.BIG_ENDIAN); + w1 = bb.getInt(); + w2 = bb.getInt(); + + boolean control = (w1 & 0x80000000) != 0; + flags = (w2 & 0xff000000) >>> 24; + length = (w2 & 0xffffff); + + if (!control) { + streamId = w1 & 0x7fffffff; + inFinished = (flags & FLAG_FIN) != 0; + emitter.setDataCallback(onDataFrame); + } + else { + reader.read(length, onFullFrame); + } + } + }; + + private final DataCallback onDataFrame = new DataCallback() { + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + int toRead = Math.min(bb.remaining(), length); + if (toRead < bb.remaining()) { + ByteBufferList partial = new ByteBufferList(); + bb.get(partial, toRead); + bb = partial; + } + + length -= toRead; + handler.data(length == 0 && inFinished, streamId, bb); + + if (length == 0) + parseFrameHeader(); + } + }; + + private final DataCallback onFullFrame = new DataCallback() { + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + // queue up the next frame read + bb.order(ByteOrder.BIG_ENDIAN); + + int version = (w1 & 0x7fff0000) >>> 16; + int type = (w1 & 0xffff); + + try { + if (version != 3) { + throw new ProtocolException("version != 3: " + version); + } + + switch (type) { + case TYPE_SYN_STREAM: + readSynStream(bb, flags, length); + break; + + case TYPE_SYN_REPLY: + readSynReply(bb, flags, length); + break; + + case TYPE_RST_STREAM: + readRstStream(bb, flags, length); + break; + + case TYPE_SETTINGS: + readSettings(bb, flags, length); + break; + + case TYPE_PING: + readPing(bb, flags, length); + break; + + case TYPE_GOAWAY: + readGoAway(bb, flags, length); + break; + + case TYPE_HEADERS: + readHeaders(bb, flags, length); + break; + + case TYPE_WINDOW_UPDATE: + readWindowUpdate(bb, flags, length); + break; + + default: + bb.recycle(); + break; + } + parseFrameHeader(); + } + catch (IOException e) { + handler.error(e); + } + } + }; + + @Override + public void readConnectionPreface() { + } + private void readSynStream(ByteBufferList source, int flags, int length) throws IOException { + int w1 = source.getInt(); + int w2 = source.getInt(); + int streamId = w1 & 0x7fffffff; + int associatedStreamId = w2 & 0x7fffffff; + source.getShort(); // int priority = (s3 & 0xe000) >>> 13; int slot = s3 & 0xff; + List
headerBlock = headerReader.readHeader(source, length - 10); + + boolean inFinished = (flags & FLAG_FIN) != 0; + boolean outFinished = (flags & FLAG_UNIDIRECTIONAL) != 0; + handler.headers(outFinished, inFinished, streamId, associatedStreamId, headerBlock, + HeadersMode.SPDY_SYN_STREAM); + } - 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 = source.readInt(); - int w2 = source.readInt(); - int streamId = w1 & 0x7fffffff; - long increment = w2 & 0x7fffffff; - if (increment == 0) throw ioException("windowSizeIncrement was 0", increment); - handler.windowUpdate(streamId, increment); - } + private void readSynReply(ByteBufferList source, int flags, int length) throws IOException { + int w1 = source.getInt(); + int streamId = w1 & 0x7fffffff; + List
headerBlock = headerReader.readHeader(source, length - 4); + boolean inFinished = (flags & FLAG_FIN) != 0; + handler.headers(false, inFinished, streamId, -1, headerBlock, HeadersMode.SPDY_REPLY); + } - private void readPing(Handler handler, int flags, int length) throws IOException { - if (length != 4) throw ioException("TYPE_PING length: %d != 4", length); - int id = source.readInt(); - boolean ack = client == ((id & 1) == 1); - handler.ping(ack, id, 0); - } + private void readRstStream(ByteBufferList source, int flags, int length) throws IOException { + if (length != 8) throw ioException("TYPE_RST_STREAM length: %d != 8", length); + int streamId = source.getInt() & 0x7fffffff; + int errorCodeInt = source.getInt(); + ErrorCode errorCode = ErrorCode.fromSpdy3Rst(errorCodeInt); + if (errorCode == null) { + throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt); + } + handler.rstStream(streamId, errorCode); + } - private void readGoAway(Handler handler, int flags, int length) throws IOException { - if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length); - int lastGoodStreamId = source.readInt() & 0x7fffffff; - int errorCodeInt = source.readInt(); - ErrorCode errorCode = ErrorCode.fromSpdyGoAway(errorCodeInt); - if (errorCode == null) { - throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt); - } - handler.goAway(lastGoodStreamId, errorCode, ByteString.EMPTY); - } + private void readHeaders(ByteBufferList source, int flags, int length) throws IOException { + int w1 = source.getInt(); + int streamId = w1 & 0x7fffffff; + List
headerBlock = headerReader.readHeader(source, length - 4); + handler.headers(false, false, streamId, -1, headerBlock, HeadersMode.SPDY_HEADERS); + } - private void readSettings(Handler handler, int flags, int length) throws IOException { - int numberOfEntries = source.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 = source.readInt(); - int value = source.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 void readWindowUpdate(ByteBufferList source, int flags, int length) throws IOException { + if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length); + int w1 = source.getInt(); + int w2 = source.getInt(); + int streamId = w1 & 0x7fffffff; + long increment = w2 & 0x7fffffff; + if (increment == 0) throw ioException("windowSizeIncrement was 0", increment); + handler.windowUpdate(streamId, increment); + } - private static IOException ioException(String message, Object... args) throws IOException { - throw new IOException(String.format(message, args)); - } + private void readPing(ByteBufferList source, int flags, int length) throws IOException { + if (length != 4) throw ioException("TYPE_PING length: %d != 4", length); + int id = source.getInt(); + boolean ack = client == ((id & 1) == 1); + handler.ping(ack, id, 0); + } - @Override public void close() throws IOException { - headerBlockReader.close(); - } - } - - /** Write spdy/3 frames. */ - static final class Writer implements FrameWriter { - private final BufferedSink sink; - private final Buffer headerBlockBuffer; - private final BufferedSink headerBlockOut; - private final boolean client; - private boolean closed; - - Writer(BufferedSink sink, boolean client) { - this.sink = sink; - this.client = client; - - Deflater deflater = new Deflater(); - deflater.setDictionary(DICTIONARY); - headerBlockBuffer = new Buffer(); - headerBlockOut = Okio.buffer(new DeflaterSink(headerBlockBuffer, deflater)); - } + private void readGoAway(ByteBufferList source, int flags, int length) throws IOException { + if (length != 8) throw ioException("TYPE_GOAWAY length: %d != 8", length); + int lastGoodStreamId = source.getInt() & 0x7fffffff; + int errorCodeInt = source.getInt(); + ErrorCode errorCode = ErrorCode.fromSpdyGoAway(errorCodeInt); + if (errorCode == null) { + throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt); + } + handler.goAway(lastGoodStreamId, errorCode, ByteString.EMPTY); + } + + private void readSettings(ByteBufferList source, int flags, int length) throws IOException { + int numberOfEntries = source.getInt(); + 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 = source.getInt(); + int value = source.getInt(); + 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 ackSettings() { - // Do nothing: no ACK for SPDY/3 settings. + @Override + public void close() throws IOException { + } } - @Override - public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) + /** + * Write spdy/3 frames. + */ + static final class Writer implements FrameWriter { + private final BufferedSink sink; + private final Buffer headerBlockBuffer; + private final BufferedSink headerBlockOut; + private final boolean client; + private boolean closed; + + Writer(BufferedSink sink, boolean client) { + this.sink = sink; + this.client = client; + + Deflater deflater = new Deflater(); + deflater.setDictionary(DICTIONARY); + headerBlockBuffer = new Buffer(); + headerBlockOut = Okio.buffer(new DeflaterSink(headerBlockBuffer, deflater)); + } + + @Override + public void ackSettings() { + // Do nothing: no ACK for SPDY/3 settings. + } + + @Override + public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) throws IOException { - // Do nothing: no push promise for SPDY/3. - } + // Do nothing: no push promise for SPDY/3. + } - @Override public synchronized void connectionPreface() { - // Do nothing: no connection preface for SPDY/3. - } + @Override + public synchronized void connectionPreface() { + // Do nothing: no connection preface for SPDY/3. + } - @Override public synchronized void flush() throws IOException { - if (closed) throw new IOException("closed"); - sink.flush(); - } + @Override + public synchronized void flush() throws IOException { + if (closed) throw new IOException("closed"); + sink.flush(); + } - @Override public synchronized void synStream(boolean outFinished, boolean inFinished, - int streamId, int associatedStreamId, List
headerBlock) + @Override + public synchronized void synStream(boolean outFinished, boolean inFinished, + int streamId, int associatedStreamId, List
headerBlock) throws IOException { - if (closed) throw new IOException("closed"); - writeNameValueBlockToBuffer(headerBlock); - int length = (int) (10 + headerBlockBuffer.size()); - int type = TYPE_SYN_STREAM; - int flags = (outFinished ? FLAG_FIN : 0) | (inFinished ? FLAG_UNIDIRECTIONAL : 0); - - int unused = 0; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId & 0x7fffffff); - sink.writeInt(associatedStreamId & 0x7fffffff); - sink.writeShort((unused & 0x7) << 13 | (unused & 0x1f) << 8 | (unused & 0xff)); - sink.writeAll(headerBlockBuffer); - sink.flush(); - } + if (closed) throw new IOException("closed"); + writeNameValueBlockToBuffer(headerBlock); + int length = (int) (10 + headerBlockBuffer.size()); + int type = TYPE_SYN_STREAM; + int flags = (outFinished ? FLAG_FIN : 0) | (inFinished ? FLAG_UNIDIRECTIONAL : 0); + + int unused = 0; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeInt(associatedStreamId & 0x7fffffff); + sink.writeShort((unused & 0x7) << 13 | (unused & 0x1f) << 8 | (unused & 0xff)); + sink.writeAll(headerBlockBuffer); + sink.flush(); + } - @Override public synchronized void synReply(boolean outFinished, int streamId, - List
headerBlock) throws IOException { - if (closed) throw new IOException("closed"); - writeNameValueBlockToBuffer(headerBlock); - int type = TYPE_SYN_REPLY; - int flags = (outFinished ? FLAG_FIN : 0); - int length = (int) (headerBlockBuffer.size() + 4); - - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId & 0x7fffffff); - sink.writeAll(headerBlockBuffer); - sink.flush(); - } + @Override + public synchronized void synReply(boolean outFinished, int streamId, + List
headerBlock) throws IOException { + if (closed) throw new IOException("closed"); + writeNameValueBlockToBuffer(headerBlock); + int type = TYPE_SYN_REPLY; + int flags = (outFinished ? FLAG_FIN : 0); + int length = (int) (headerBlockBuffer.size() + 4); + + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeAll(headerBlockBuffer); + sink.flush(); + } - @Override public synchronized void headers(int streamId, List
headerBlock) + @Override + public synchronized void headers(int streamId, List
headerBlock) throws IOException { - if (closed) throw new IOException("closed"); - writeNameValueBlockToBuffer(headerBlock); - int flags = 0; - int type = TYPE_HEADERS; - int length = (int) (headerBlockBuffer.size() + 4); - - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId & 0x7fffffff); - sink.writeAll(headerBlockBuffer); - } + if (closed) throw new IOException("closed"); + writeNameValueBlockToBuffer(headerBlock); + int flags = 0; + int type = TYPE_HEADERS; + int length = (int) (headerBlockBuffer.size() + 4); + + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeAll(headerBlockBuffer); + } - @Override public synchronized void rstStream(int streamId, ErrorCode errorCode) + @Override + public synchronized void rstStream(int streamId, ErrorCode errorCode) throws IOException { - if (closed) throw new IOException("closed"); - if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException(); - int flags = 0; - int type = TYPE_RST_STREAM; - int length = 8; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId & 0x7fffffff); - sink.writeInt(errorCode.spdyRstCode); - sink.flush(); - } + if (closed) throw new IOException("closed"); + if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException(); + int flags = 0; + int type = TYPE_RST_STREAM; + int length = 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId & 0x7fffffff); + sink.writeInt(errorCode.spdyRstCode); + sink.flush(); + } - @Override public synchronized void data(boolean outFinished, int streamId, Buffer source) + @Override + public synchronized void data(boolean outFinished, int streamId, Buffer source) throws IOException { - data(outFinished, streamId, source, (int) source.size()); - } + data(outFinished, streamId, source, (int) source.size()); + } - @Override public synchronized void data(boolean outFinished, int streamId, Buffer source, - int byteCount) throws IOException { - int flags = (outFinished ? FLAG_FIN : 0); - sendDataFrame(streamId, flags, source, byteCount); - } + @Override + public synchronized void data(boolean outFinished, int streamId, Buffer source, + int byteCount) throws IOException { + int flags = (outFinished ? FLAG_FIN : 0); + sendDataFrame(streamId, flags, source, byteCount); + } - void sendDataFrame(int streamId, int flags, Buffer buffer, int byteCount) + void sendDataFrame(int streamId, int flags, Buffer buffer, int byteCount) throws IOException { - if (closed) throw new IOException("closed"); - if (byteCount > 0xffffffL) { - throw new IllegalArgumentException("FRAME_TOO_LARGE max size is 16Mib: " + byteCount); - } - sink.writeInt(streamId & 0x7fffffff); - sink.writeInt((flags & 0xff) << 24 | byteCount & 0xffffff); - if (byteCount > 0) { - sink.write(buffer, byteCount); - } - } + if (closed) throw new IOException("closed"); + if (byteCount > 0xffffffL) { + throw new IllegalArgumentException("FRAME_TOO_LARGE max size is 16Mib: " + byteCount); + } + sink.writeInt(streamId & 0x7fffffff); + sink.writeInt((flags & 0xff) << 24 | byteCount & 0xffffff); + if (byteCount > 0) { + sink.write(buffer, byteCount); + } + } - private void writeNameValueBlockToBuffer(List
headerBlock) throws IOException { - if (headerBlockBuffer.size() != 0) throw new IllegalStateException(); - headerBlockOut.writeInt(headerBlock.size()); - for (int i = 0, size = headerBlock.size(); i < size; i++) { - ByteString name = headerBlock.get(i).name; - headerBlockOut.writeInt(name.size()); - headerBlockOut.write(name); - ByteString value = headerBlock.get(i).value; - headerBlockOut.writeInt(value.size()); - headerBlockOut.write(value); - } - headerBlockOut.flush(); - } + private void writeNameValueBlockToBuffer(List
headerBlock) throws IOException { + if (headerBlockBuffer.size() != 0) throw new IllegalStateException(); + headerBlockOut.writeInt(headerBlock.size()); + for (int i = 0, size = headerBlock.size(); i < size; i++) { + ByteString name = headerBlock.get(i).name; + headerBlockOut.writeInt(name.size()); + headerBlockOut.write(name); + ByteString value = headerBlock.get(i).value; + headerBlockOut.writeInt(value.size()); + headerBlockOut.write(value); + } + headerBlockOut.flush(); + } - @Override public synchronized void settings(Settings settings) throws IOException { - if (closed) throw new IOException("closed"); - int type = TYPE_SETTINGS; - int flags = 0; - int size = settings.size(); - int length = 4 + size * 8; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(size); - for (int i = 0; i <= Settings.COUNT; i++) { - if (!settings.isSet(i)) continue; - int settingsFlags = settings.flags(i); - sink.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff)); - sink.writeInt(settings.get(i)); - } - sink.flush(); - } + @Override + public synchronized void settings(Settings settings) throws IOException { + if (closed) throw new IOException("closed"); + int type = TYPE_SETTINGS; + int flags = 0; + int size = settings.size(); + int length = 4 + size * 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(size); + for (int i = 0; i <= Settings.COUNT; i++) { + if (!settings.isSet(i)) continue; + int settingsFlags = settings.flags(i); + sink.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff)); + sink.writeInt(settings.get(i)); + } + sink.flush(); + } - @Override public synchronized void ping(boolean reply, int payload1, int payload2) + @Override + public synchronized void ping(boolean reply, int payload1, int payload2) throws IOException { - if (closed) throw new IOException("closed"); - boolean payloadIsReply = client != ((payload1 & 1) == 1); - if (reply != payloadIsReply) throw new IllegalArgumentException("payload != reply"); - int type = TYPE_PING; - int flags = 0; - int length = 4; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(payload1); - sink.flush(); - } + if (closed) throw new IOException("closed"); + boolean payloadIsReply = client != ((payload1 & 1) == 1); + if (reply != payloadIsReply) throw new IllegalArgumentException("payload != reply"); + int type = TYPE_PING; + int flags = 0; + int length = 4; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(payload1); + sink.flush(); + } - @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, - byte[] ignored) throws IOException { - if (closed) throw new IOException("closed"); - if (errorCode.spdyGoAwayCode == -1) { - throw new IllegalArgumentException("errorCode.spdyGoAwayCode == -1"); - } - int type = TYPE_GOAWAY; - int flags = 0; - int length = 8; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(lastGoodStreamId); - sink.writeInt(errorCode.spdyGoAwayCode); - sink.flush(); - } + @Override + public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, + byte[] ignored) throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.spdyGoAwayCode == -1) { + throw new IllegalArgumentException("errorCode.spdyGoAwayCode == -1"); + } + int type = TYPE_GOAWAY; + int flags = 0; + int length = 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(lastGoodStreamId); + sink.writeInt(errorCode.spdyGoAwayCode); + sink.flush(); + } - @Override public synchronized void windowUpdate(int streamId, long increment) + @Override + public synchronized void windowUpdate(int streamId, long increment) throws IOException { - if (closed) throw new IOException("closed"); - if (increment == 0 || increment > 0x7fffffffL) { - throw new IllegalArgumentException( - "windowSizeIncrement must be between 1 and 0x7fffffff: " + increment); - } - int type = TYPE_WINDOW_UPDATE; - int flags = 0; - int length = 8; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId); - sink.writeInt((int) increment); - sink.flush(); - } + if (closed) throw new IOException("closed"); + if (increment == 0 || increment > 0x7fffffffL) { + throw new IllegalArgumentException( + "windowSizeIncrement must be between 1 and 0x7fffffff: " + increment); + } + int type = TYPE_WINDOW_UPDATE; + int flags = 0; + int length = 8; + sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); + sink.writeInt(streamId); + sink.writeInt((int) increment); + sink.flush(); + } - @Override public synchronized void close() throws IOException { - closed = true; - Util.closeAll(sink, headerBlockOut); + @Override + public synchronized void close() throws IOException { + closed = true; + Util.closeAll(sink, headerBlockOut); + } } - } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java index 56994d177..eeb7c157e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java @@ -16,9 +16,9 @@ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.spdy.okio.BufferedSink; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; /** A version and dialect of the framed socket protocol. */ public interface Variant { @@ -29,7 +29,7 @@ public interface Variant { /** * @param client true if this is the HTTP client's reader, reading frames from a server. */ - FrameReader newReader(BufferedSource source, boolean client); + FrameReader newReader(DataEmitter source, FrameReader.Handler handler, boolean client); /** * @param client true if this is the HTTP client's writer, writing frames to a server. From 73481c3360e2ba7421c7ff61586b152f6dfeb75e Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 00:16:10 -0700 Subject: [PATCH 051/399] h2-13 refactor without okio. totally untested. --- AndroidAsync/AndroidAsync-AndroidAsync.iml | 1 - .../koushikdutta/async/ByteBufferList.java | 14 +- .../async/http/spdy/AsyncSpdyConnection.java | 4 +- .../okhttp/internal/spdy/FrameReader.java | 4 +- .../okhttp/internal/spdy/HeaderReader.java | 4 +- .../okhttp/internal/spdy/HpackDraft08.java | 821 +++++++++--------- .../internal/spdy/Http20Draft13.java.ignore | 763 ---------------- .../http/spdy/okhttp/internal/spdy/Spdy3.java | 8 +- 8 files changed, 439 insertions(+), 1180 deletions(-) delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java.ignore diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index 83bc4a321..f88736b56 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -57,7 +57,6 @@ - diff --git a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java index a27f47100..84c33074b 100644 --- a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java +++ b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java @@ -46,6 +46,12 @@ public void addAll(ByteBuffer... bb) { add(b); } + public byte[] getBytes(int length) { + byte[] ret = new byte[length]; + get(ret); + return ret; + } + public byte[] getAllByteArray() { // fast path to return the contents of the first and only byte buffer, // if that's what we're looking for. avoids allocation. @@ -102,6 +108,11 @@ public byte[] peekBytes(int size) { return ret; } + public ByteBufferList skip(int length) { + get(null, 0, length); + return this; + } + public int getInt() { int ret = read(4).getInt(); remaining -= 4; @@ -144,7 +155,8 @@ public void get(byte[] bytes, int offset, int length) { while (need > 0) { ByteBuffer b = mBuffers.peek(); int read = Math.min(b.remaining(), need); - b.get(bytes, offset, read); + if (bytes != null) + b.get(bytes, offset, read); need -= read; offset += read; if (b.remaining() == 0) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index d9feaf708..05a546ed6 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -16,6 +16,7 @@ import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameWriter; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.HeadersMode; +import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Http20Draft13; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Ping; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3; @@ -281,8 +282,7 @@ public AsyncSpdyConnection(AsyncSocket socket, Protocol protocol) { variant = new Spdy3(); } else if (protocol == Protocol.HTTP_2) { - throw new AssertionError("http20draft13"); -// variant = new Http20Draft13(); + variant = new Http20Draft13(); } reader = variant.newReader(socket, this, true); writer = variant.newWriter(bufferedSink = Okio.buffer(sink), true); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java index 3f457fb0c..087182126 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java @@ -26,8 +26,8 @@ /** * Reads transport frames for SPDY/3 or HTTP/2. */ -public interface FrameReader extends Closeable { - void readConnectionPreface() throws IOException; +public interface FrameReader { +// void readConnectionPreface() throws IOException; // boolean nextFrame(Handler handler) throws IOException; public interface Handler { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java index 618f7053b..b4aa5dcbd 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java @@ -62,8 +62,6 @@ public List
readHeader(ByteBufferList bb, int length) throws IOException private static ByteString readByteString(ByteBufferList source) { int length = source.getInt(); - byte[] bytes = new byte[length]; - source.get(bytes); - return ByteString.of(bytes); + return ByteString.of(source.getBytes(length)); } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java index 397736f71..f578cb225 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java @@ -15,6 +15,7 @@ */ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.http.spdy.okhttp.internal.BitArray; import com.koushikdutta.async.http.spdy.okio.Buffer; import com.koushikdutta.async.http.spdy.okio.BufferedSource; @@ -32,460 +33,472 @@ /** * Read and write HPACK v08. - * + *

* http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08 - * + *

* This implementation uses an array for the header table with a bitset for * references. Dynamic entries are added to the array, starting in the last * position moving forward. When the array fills, it is doubled. */ final class HpackDraft08 { - private static final int PREFIX_4_BITS = 0x0f; - private static final int PREFIX_6_BITS = 0x3f; - private static final int PREFIX_7_BITS = 0x7f; - - private static final Header[] STATIC_HEADER_TABLE = new Header[] { - new Header(Header.TARGET_AUTHORITY, ""), - new Header(Header.TARGET_METHOD, "GET"), - new Header(Header.TARGET_METHOD, "POST"), - new Header(Header.TARGET_PATH, "/"), - new Header(Header.TARGET_PATH, "/index.html"), - new Header(Header.TARGET_SCHEME, "http"), - new Header(Header.TARGET_SCHEME, "https"), - new Header(Header.RESPONSE_STATUS, "200"), - new Header(Header.RESPONSE_STATUS, "204"), - new Header(Header.RESPONSE_STATUS, "206"), - new Header(Header.RESPONSE_STATUS, "304"), - new Header(Header.RESPONSE_STATUS, "400"), - new Header(Header.RESPONSE_STATUS, "404"), - new Header(Header.RESPONSE_STATUS, "500"), - new Header("accept-charset", ""), - new Header("accept-encoding", "gzip, deflate"), - new Header("accept-language", ""), - new Header("accept-ranges", ""), - new Header("accept", ""), - new Header("access-control-allow-origin", ""), - new Header("age", ""), - new Header("allow", ""), - new Header("authorization", ""), - new Header("cache-control", ""), - new Header("content-disposition", ""), - new Header("content-encoding", ""), - new Header("content-language", ""), - new Header("content-length", ""), - new Header("content-location", ""), - new Header("content-range", ""), - new Header("content-type", ""), - new Header("cookie", ""), - new Header("date", ""), - new Header("etag", ""), - new Header("expect", ""), - new Header("expires", ""), - new Header("from", ""), - new Header("host", ""), - new Header("if-match", ""), - new Header("if-modified-since", ""), - new Header("if-none-match", ""), - new Header("if-range", ""), - new Header("if-unmodified-since", ""), - new Header("last-modified", ""), - new Header("link", ""), - new Header("location", ""), - new Header("max-forwards", ""), - new Header("proxy-authenticate", ""), - new Header("proxy-authorization", ""), - new Header("range", ""), - new Header("referer", ""), - new Header("refresh", ""), - new Header("retry-after", ""), - new Header("server", ""), - new Header("set-cookie", ""), - new Header("strict-transport-security", ""), - new Header("transfer-encoding", ""), - new Header("user-agent", ""), - new Header("vary", ""), - new Header("via", ""), - new Header("www-authenticate", "") - }; - - private HpackDraft08() { - } - - // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-3.2 - static final class Reader { - - private final List

emittedHeaders = new ArrayList
(); - private final BufferedSource source; - - private int maxHeaderTableByteCountSetting; - private int maxHeaderTableByteCount; - // Visible for testing. - Header[] headerTable = new Header[8]; - // Array is populated back to front, so new entries always have lowest index. - int nextHeaderIndex = headerTable.length - 1; - int headerCount = 0; + private static final int PREFIX_4_BITS = 0x0f; + private static final int PREFIX_6_BITS = 0x3f; + private static final int PREFIX_7_BITS = 0x7f; + + private static final Header[] STATIC_HEADER_TABLE = new Header[]{ + new Header(Header.TARGET_AUTHORITY, ""), + new Header(Header.TARGET_METHOD, "GET"), + new Header(Header.TARGET_METHOD, "POST"), + new Header(Header.TARGET_PATH, "/"), + new Header(Header.TARGET_PATH, "/index.html"), + new Header(Header.TARGET_SCHEME, "http"), + new Header(Header.TARGET_SCHEME, "https"), + new Header(Header.RESPONSE_STATUS, "200"), + new Header(Header.RESPONSE_STATUS, "204"), + new Header(Header.RESPONSE_STATUS, "206"), + new Header(Header.RESPONSE_STATUS, "304"), + new Header(Header.RESPONSE_STATUS, "400"), + new Header(Header.RESPONSE_STATUS, "404"), + new Header(Header.RESPONSE_STATUS, "500"), + new Header("accept-charset", ""), + new Header("accept-encoding", "gzip, deflate"), + new Header("accept-language", ""), + new Header("accept-ranges", ""), + new Header("accept", ""), + new Header("access-control-allow-origin", ""), + new Header("age", ""), + new Header("allow", ""), + new Header("authorization", ""), + new Header("cache-control", ""), + new Header("content-disposition", ""), + new Header("content-encoding", ""), + new Header("content-language", ""), + new Header("content-length", ""), + new Header("content-location", ""), + new Header("content-range", ""), + new Header("content-type", ""), + new Header("cookie", ""), + new Header("date", ""), + new Header("etag", ""), + new Header("expect", ""), + new Header("expires", ""), + new Header("from", ""), + new Header("host", ""), + new Header("if-match", ""), + new Header("if-modified-since", ""), + new Header("if-none-match", ""), + new Header("if-range", ""), + new Header("if-unmodified-since", ""), + new Header("last-modified", ""), + new Header("link", ""), + new Header("location", ""), + new Header("max-forwards", ""), + new Header("proxy-authenticate", ""), + new Header("proxy-authorization", ""), + new Header("range", ""), + new Header("referer", ""), + new Header("refresh", ""), + new Header("retry-after", ""), + new Header("server", ""), + new Header("set-cookie", ""), + new Header("strict-transport-security", ""), + new Header("transfer-encoding", ""), + new Header("user-agent", ""), + new Header("vary", ""), + new Header("via", ""), + new Header("www-authenticate", "") + }; + + private HpackDraft08() { + } - /** - * Set bit positions indicate {@code headerTable[pos]} should be emitted. - */ - // Using a BitArray as it has left-shift operator. - BitArray referencedHeaders = new BitArray.FixedCapacity(); + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-3.2 + static final class Reader { + + private final List
emittedHeaders = new ArrayList
(); + private final ByteBufferList source = new ByteBufferList(); + + private int maxHeaderTableByteCountSetting; + private int maxHeaderTableByteCount; + // Visible for testing. + Header[] headerTable = new Header[8]; + // Array is populated back to front, so new entries always have lowest index. + int nextHeaderIndex = headerTable.length - 1; + int headerCount = 0; + + /** + * Set bit positions indicate {@code headerTable[pos]} should be emitted. + */ + // Using a BitArray as it has left-shift operator. + BitArray referencedHeaders = new BitArray.FixedCapacity(); + + /** + * Set bit positions indicate {@code headerTable[pos]} was already emitted. + */ + BitArray emittedReferencedHeaders = new BitArray.FixedCapacity(); + int headerTableByteCount = 0; + + Reader(int maxHeaderTableByteCountSetting) { + this.maxHeaderTableByteCountSetting = maxHeaderTableByteCountSetting; + this.maxHeaderTableByteCount = maxHeaderTableByteCountSetting; + } - /** - * Set bit positions indicate {@code headerTable[pos]} was already emitted. - */ - BitArray emittedReferencedHeaders = new BitArray.FixedCapacity(); - int headerTableByteCount = 0; + public void refill(ByteBufferList bb) { + bb.get(source); + } - Reader(int maxHeaderTableByteCountSetting, Source source) { - this.maxHeaderTableByteCountSetting = maxHeaderTableByteCountSetting; - this.maxHeaderTableByteCount = maxHeaderTableByteCountSetting; - this.source = Okio.buffer(source); - } + int maxHeaderTableByteCount() { + return maxHeaderTableByteCount; + } - int maxHeaderTableByteCount() { - return maxHeaderTableByteCount; - } + /** + * Called by the reader when the peer sent a new header table size setting. + * While this establishes the maximum header table size, the + * {@link #maxHeaderTableByteCount} set during processing may limit the + * table size to a smaller amount. + *

Evicts entries or clears the table as needed. + */ + void maxHeaderTableByteCountSetting(int newMaxHeaderTableByteCountSetting) { + this.maxHeaderTableByteCountSetting = newMaxHeaderTableByteCountSetting; + this.maxHeaderTableByteCount = maxHeaderTableByteCountSetting; + adjustHeaderTableByteCount(); + } - /** - * Called by the reader when the peer sent a new header table size setting. - * While this establishes the maximum header table size, the - * {@link #maxHeaderTableByteCount} set during processing may limit the - * table size to a smaller amount. - *

Evicts entries or clears the table as needed. - */ - void maxHeaderTableByteCountSetting(int newMaxHeaderTableByteCountSetting) { - this.maxHeaderTableByteCountSetting = newMaxHeaderTableByteCountSetting; - this.maxHeaderTableByteCount = maxHeaderTableByteCountSetting; - adjustHeaderTableByteCount(); - } + private void adjustHeaderTableByteCount() { + if (maxHeaderTableByteCount < headerTableByteCount) { + if (maxHeaderTableByteCount == 0) { + clearHeaderTable(); + } else { + evictToRecoverBytes(headerTableByteCount - maxHeaderTableByteCount); + } + } + } - private void adjustHeaderTableByteCount() { - if (maxHeaderTableByteCount < headerTableByteCount) { - if (maxHeaderTableByteCount == 0) { - clearHeaderTable(); - } else { - evictToRecoverBytes(headerTableByteCount - maxHeaderTableByteCount); + private void clearHeaderTable() { + clearReferenceSet(); + Arrays.fill(headerTable, null); + nextHeaderIndex = headerTable.length - 1; + headerCount = 0; + headerTableByteCount = 0; } - } - } - private void clearHeaderTable() { - clearReferenceSet(); - Arrays.fill(headerTable, null); - nextHeaderIndex = headerTable.length - 1; - headerCount = 0; - headerTableByteCount = 0; - } + /** + * Returns the count of entries evicted. + */ + private int evictToRecoverBytes(int bytesToRecover) { + int entriesToEvict = 0; + if (bytesToRecover > 0) { + // determine how many headers need to be evicted. + for (int j = headerTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) { + bytesToRecover -= headerTable[j].hpackSize; + headerTableByteCount -= headerTable[j].hpackSize; + headerCount--; + entriesToEvict++; + } + referencedHeaders.shiftLeft(entriesToEvict); + emittedReferencedHeaders.shiftLeft(entriesToEvict); + System.arraycopy(headerTable, nextHeaderIndex + 1, headerTable, + nextHeaderIndex + 1 + entriesToEvict, headerCount); + nextHeaderIndex += entriesToEvict; + } + return entriesToEvict; + } - /** Returns the count of entries evicted. */ - private int evictToRecoverBytes(int bytesToRecover) { - int entriesToEvict = 0; - if (bytesToRecover > 0) { - // determine how many headers need to be evicted. - for (int j = headerTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) { - bytesToRecover -= headerTable[j].hpackSize; - headerTableByteCount -= headerTable[j].hpackSize; - headerCount--; - entriesToEvict++; + /** + * Read {@code byteCount} bytes of headers from the source stream into the + * set of emitted headers. This implementation does not propagate the never + * indexed flag of a header. + */ + void readHeaders() throws IOException { + while (source.hasRemaining()) { + int b = source.get() & 0xff; + if (b == 0x80) { // 10000000 + throw new IOException("index == 0"); + } else if ((b & 0x80) == 0x80) { // 1NNNNNNN + int index = readInt(b, PREFIX_7_BITS); + readIndexedHeader(index - 1); + } else if (b == 0x40) { // 01000000 + readLiteralHeaderWithIncrementalIndexingNewName(); + } else if ((b & 0x40) == 0x40) { // 01NNNNNN + int index = readInt(b, PREFIX_6_BITS); + readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); + } else if ((b & 0x20) == 0x20) { // 001NNNNN + if ((b & 0x10) == 0x10) { // 0011NNNN + if ((b & 0x0f) != 0) + throw new IOException("Invalid header table state change " + b); + clearReferenceSet(); // 00110000 + } else { // 0010NNNN + maxHeaderTableByteCount = readInt(b, PREFIX_4_BITS); + if (maxHeaderTableByteCount < 0 + || maxHeaderTableByteCount > maxHeaderTableByteCountSetting) { + throw new IOException("Invalid header table byte count " + maxHeaderTableByteCount); + } + adjustHeaderTableByteCount(); + } + } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit. + readLiteralHeaderWithoutIndexingNewName(); + } else { // 000?NNNN - Ignore never indexed bit. + int index = readInt(b, PREFIX_4_BITS); + readLiteralHeaderWithoutIndexingIndexedName(index - 1); + } + } } - referencedHeaders.shiftLeft(entriesToEvict); - emittedReferencedHeaders.shiftLeft(entriesToEvict); - System.arraycopy(headerTable, nextHeaderIndex + 1, headerTable, - nextHeaderIndex + 1 + entriesToEvict, headerCount); - nextHeaderIndex += entriesToEvict; - } - return entriesToEvict; - } - /** - * Read {@code byteCount} bytes of headers from the source stream into the - * set of emitted headers. This implementation does not propagate the never - * indexed flag of a header. - */ - void readHeaders() throws IOException { - while (!source.exhausted()) { - int b = source.readByte() & 0xff; - if (b == 0x80) { // 10000000 - throw new IOException("index == 0"); - } else if ((b & 0x80) == 0x80) { // 1NNNNNNN - int index = readInt(b, PREFIX_7_BITS); - readIndexedHeader(index - 1); - } else if (b == 0x40) { // 01000000 - readLiteralHeaderWithIncrementalIndexingNewName(); - } else if ((b & 0x40) == 0x40) { // 01NNNNNN - int index = readInt(b, PREFIX_6_BITS); - readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1); - } else if ((b & 0x20) == 0x20) { // 001NNNNN - if ((b & 0x10) == 0x10) { // 0011NNNN - if ((b & 0x0f) != 0) throw new IOException("Invalid header table state change " + b); - clearReferenceSet(); // 00110000 - } else { // 0010NNNN - maxHeaderTableByteCount = readInt(b, PREFIX_4_BITS); - if (maxHeaderTableByteCount < 0 - || maxHeaderTableByteCount > maxHeaderTableByteCountSetting) { - throw new IOException("Invalid header table byte count " + maxHeaderTableByteCount); + private void clearReferenceSet() { + referencedHeaders.clear(); + emittedReferencedHeaders.clear(); + } + + void emitReferenceSet() { + for (int i = headerTable.length - 1; i != nextHeaderIndex; --i) { + if (referencedHeaders.get(i) && !emittedReferencedHeaders.get(i)) { + emittedHeaders.add(headerTable[i]); + } } - adjustHeaderTableByteCount(); - } - } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit. - readLiteralHeaderWithoutIndexingNewName(); - } else { // 000?NNNN - Ignore never indexed bit. - int index = readInt(b, PREFIX_4_BITS); - readLiteralHeaderWithoutIndexingIndexedName(index - 1); } - } - } - private void clearReferenceSet() { - referencedHeaders.clear(); - emittedReferencedHeaders.clear(); - } + /** + * Returns all headers emitted since they were last cleared, then clears the + * emitted headers. + */ + List

getAndReset() { + List
result = new ArrayList
(emittedHeaders); + emittedHeaders.clear(); + emittedReferencedHeaders.clear(); + return result; + } - void emitReferenceSet() { - for (int i = headerTable.length - 1; i != nextHeaderIndex; --i) { - if (referencedHeaders.get(i) && !emittedReferencedHeaders.get(i)) { - emittedHeaders.add(headerTable[i]); + private void readIndexedHeader(int index) throws IOException { + if (isStaticHeader(index)) { + index -= headerCount; + if (index > STATIC_HEADER_TABLE.length - 1) { + throw new IOException("Header index too large " + (index + 1)); + } + Header staticEntry = STATIC_HEADER_TABLE[index]; + if (maxHeaderTableByteCount == 0) { + emittedHeaders.add(staticEntry); + } else { + insertIntoHeaderTable(-1, staticEntry); + } + } else { + int headerTableIndex = headerTableIndex(index); + if (!referencedHeaders.get(headerTableIndex)) { // When re-referencing, emit immediately. + emittedHeaders.add(headerTable[headerTableIndex]); + emittedReferencedHeaders.set(headerTableIndex); + } + referencedHeaders.toggle(headerTableIndex); + } } - } - } - /** - * Returns all headers emitted since they were last cleared, then clears the - * emitted headers. - */ - List
getAndReset() { - List
result = new ArrayList
(emittedHeaders); - emittedHeaders.clear(); - emittedReferencedHeaders.clear(); - return result; - } + // referencedHeaders is relative to nextHeaderIndex + 1. + private int headerTableIndex(int index) { + return nextHeaderIndex + 1 + index; + } - private void readIndexedHeader(int index) throws IOException { - if (isStaticHeader(index)) { - index -= headerCount; - if (index > STATIC_HEADER_TABLE.length - 1) { - throw new IOException("Header index too large " + (index + 1)); + private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException { + ByteString name = getName(index); + ByteString value = readByteString(); + emittedHeaders.add(new Header(name, value)); } - Header staticEntry = STATIC_HEADER_TABLE[index]; - if (maxHeaderTableByteCount == 0) { - emittedHeaders.add(staticEntry); - } else { - insertIntoHeaderTable(-1, staticEntry); + + private void readLiteralHeaderWithoutIndexingNewName() throws IOException { + ByteString name = checkLowercase(readByteString()); + ByteString value = readByteString(); + emittedHeaders.add(new Header(name, value)); } - } else { - int headerTableIndex = headerTableIndex(index); - if (!referencedHeaders.get(headerTableIndex)) { // When re-referencing, emit immediately. - emittedHeaders.add(headerTable[headerTableIndex]); - emittedReferencedHeaders.set(headerTableIndex); + + private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex) + throws IOException { + ByteString name = getName(nameIndex); + ByteString value = readByteString(); + insertIntoHeaderTable(-1, new Header(name, value)); } - referencedHeaders.toggle(headerTableIndex); - } - } - // referencedHeaders is relative to nextHeaderIndex + 1. - private int headerTableIndex(int index) { - return nextHeaderIndex + 1 + index; - } + private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException { + ByteString name = checkLowercase(readByteString()); + ByteString value = readByteString(); + insertIntoHeaderTable(-1, new Header(name, value)); + } - private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException { - ByteString name = getName(index); - ByteString value = readByteString(); - emittedHeaders.add(new Header(name, value)); - } + private ByteString getName(int index) { + if (isStaticHeader(index)) { + return STATIC_HEADER_TABLE[index - headerCount].name; + } else { + return headerTable[headerTableIndex(index)].name; + } + } - private void readLiteralHeaderWithoutIndexingNewName() throws IOException { - ByteString name = checkLowercase(readByteString()); - ByteString value = readByteString(); - emittedHeaders.add(new Header(name, value)); - } + private boolean isStaticHeader(int index) { + return index >= headerCount; + } - private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex) - throws IOException { - ByteString name = getName(nameIndex); - ByteString value = readByteString(); - insertIntoHeaderTable(-1, new Header(name, value)); - } + /** + * index == -1 when new. + */ + private void insertIntoHeaderTable(int index, Header entry) { + int delta = entry.hpackSize; + if (index != -1) { // Index -1 == new header. + delta -= headerTable[headerTableIndex(index)].hpackSize; + } - private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException { - ByteString name = checkLowercase(readByteString()); - ByteString value = readByteString(); - insertIntoHeaderTable(-1, new Header(name, value)); - } + // if the new or replacement header is too big, drop all entries. + if (delta > maxHeaderTableByteCount) { + clearHeaderTable(); + // emit the large header to the callback. + emittedHeaders.add(entry); + return; + } - private ByteString getName(int index) { - if (isStaticHeader(index)) { - return STATIC_HEADER_TABLE[index - headerCount].name; - } else { - return headerTable[headerTableIndex(index)].name; - } - } + // Evict headers to the required length. + int bytesToRecover = (headerTableByteCount + delta) - maxHeaderTableByteCount; + int entriesEvicted = evictToRecoverBytes(bytesToRecover); + + if (index == -1) { // Adding a value to the header table. + if (headerCount + 1 > headerTable.length) { // Need to grow the header table. + Header[] doubled = new Header[headerTable.length * 2]; + System.arraycopy(headerTable, 0, doubled, headerTable.length, headerTable.length); + if (doubled.length == 64) { + referencedHeaders = ((BitArray.FixedCapacity) referencedHeaders).toVariableCapacity(); + emittedReferencedHeaders = + ((BitArray.FixedCapacity) emittedReferencedHeaders).toVariableCapacity(); + } + referencedHeaders.shiftLeft(headerTable.length); + emittedReferencedHeaders.shiftLeft(headerTable.length); + nextHeaderIndex = headerTable.length - 1; + headerTable = doubled; + } + index = nextHeaderIndex--; + referencedHeaders.set(index); + headerTable[index] = entry; + headerCount++; + } else { // Replace value at same position. + index += headerTableIndex(index) + entriesEvicted; + referencedHeaders.set(index); + headerTable[index] = entry; + } + headerTableByteCount += delta; + } - private boolean isStaticHeader(int index) { - return index >= headerCount; - } + private int readByte() throws IOException { + return source.get() & 0xff; + } - /** index == -1 when new. */ - private void insertIntoHeaderTable(int index, Header entry) { - int delta = entry.hpackSize; - if (index != -1) { // Index -1 == new header. - delta -= headerTable[headerTableIndex(index)].hpackSize; - } - - // if the new or replacement header is too big, drop all entries. - if (delta > maxHeaderTableByteCount) { - clearHeaderTable(); - // emit the large header to the callback. - emittedHeaders.add(entry); - return; - } - - // Evict headers to the required length. - int bytesToRecover = (headerTableByteCount + delta) - maxHeaderTableByteCount; - int entriesEvicted = evictToRecoverBytes(bytesToRecover); - - if (index == -1) { // Adding a value to the header table. - if (headerCount + 1 > headerTable.length) { // Need to grow the header table. - Header[] doubled = new Header[headerTable.length * 2]; - System.arraycopy(headerTable, 0, doubled, headerTable.length, headerTable.length); - if (doubled.length == 64) { - referencedHeaders = ((BitArray.FixedCapacity) referencedHeaders).toVariableCapacity(); - emittedReferencedHeaders = - ((BitArray.FixedCapacity) emittedReferencedHeaders).toVariableCapacity(); - } - referencedHeaders.shiftLeft(headerTable.length); - emittedReferencedHeaders.shiftLeft(headerTable.length); - nextHeaderIndex = headerTable.length - 1; - headerTable = doubled; + 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; } - index = nextHeaderIndex--; - referencedHeaders.set(index); - headerTable[index] = entry; - headerCount++; - } else { // Replace value at same position. - index += headerTableIndex(index) + entriesEvicted; - referencedHeaders.set(index); - headerTable[index] = entry; - } - headerTableByteCount += delta; - } - private int readByte() throws IOException { - return source.readByte() & 0xff; + /** + * Reads a potentially Huffman encoded byte string. + */ + ByteString readByteString() throws IOException { + int firstByte = readByte(); + boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN + int length = readInt(firstByte, PREFIX_7_BITS); + + if (huffmanDecode) { + return ByteString.of(Huffman.get().decode(source.getBytes(length))); + } else { + return ByteString.of(source.getBytes(length)); + } + } } - 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; + private static final Map NAME_TO_FIRST_INDEX = nameToFirstIndex(); + + private static Map nameToFirstIndex() { + Map result = new LinkedHashMap(STATIC_HEADER_TABLE.length); + for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) { + if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) { + result.put(STATIC_HEADER_TABLE[i].name, i); + } } - } - return result; + return Collections.unmodifiableMap(result); } - /** Reads a potentially Huffman encoded byte string. */ - ByteString readByteString() throws IOException { - int firstByte = readByte(); - boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN - int length = readInt(firstByte, PREFIX_7_BITS); - - if (huffmanDecode) { - return ByteString.of(Huffman.get().decode(source.readByteArray(length))); - } else { - return source.readByteString(length); - } - } - } + static final class Writer { + private final Buffer out; - private static final Map NAME_TO_FIRST_INDEX = nameToFirstIndex(); + Writer(Buffer out) { + this.out = out; + } - private static Map nameToFirstIndex() { - Map result = new LinkedHashMap(STATIC_HEADER_TABLE.length); - for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) { - if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) { - result.put(STATIC_HEADER_TABLE[i].name, i); - } - } - return Collections.unmodifiableMap(result); - } + /** + * This does not use "never indexed" semantics for sensitive headers. + */ + // https://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.3.3 + void writeHeaders(List
headerBlock) throws IOException { + // TODO: implement index tracking + for (int i = 0, size = headerBlock.size(); i < size; i++) { + ByteString name = headerBlock.get(i).name.toAsciiLowercase(); + Integer staticIndex = NAME_TO_FIRST_INDEX.get(name); + if (staticIndex != null) { + // Literal Header Field without Indexing - Indexed Name. + writeInt(staticIndex + 1, PREFIX_4_BITS, 0); + writeByteString(headerBlock.get(i).value); + } else { + out.writeByte(0x00); // Literal Header without Indexing - New Name. + writeByteString(name); + writeByteString(headerBlock.get(i).value); + } + } + } - static final class Writer { - private final Buffer out; + // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.1.1 + void writeInt(int value, int prefixMask, int bits) throws IOException { + // Write the raw value for a single byte value. + if (value < prefixMask) { + out.writeByte(bits | value); + return; + } - Writer(Buffer out) { - this.out = out; - } + // Write the mask to start a multibyte value. + out.writeByte(bits | prefixMask); + value -= prefixMask; - /** This does not use "never indexed" semantics for sensitive headers. */ - // https://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.3.3 - void writeHeaders(List
headerBlock) throws IOException { - // TODO: implement index tracking - for (int i = 0, size = headerBlock.size(); i < size; i++) { - ByteString name = headerBlock.get(i).name.toAsciiLowercase(); - Integer staticIndex = NAME_TO_FIRST_INDEX.get(name); - if (staticIndex != null) { - // Literal Header Field without Indexing - Indexed Name. - writeInt(staticIndex + 1, PREFIX_4_BITS, 0); - writeByteString(headerBlock.get(i).value); - } else { - out.writeByte(0x00); // Literal Header without Indexing - New Name. - writeByteString(name); - writeByteString(headerBlock.get(i).value); + // Write 7 bits at a time 'til we're done. + while (value >= 0x80) { + int b = value & 0x7f; + out.writeByte(b | 0x80); + value >>>= 7; + } + out.writeByte(value); } - } - } - // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.1.1 - void writeInt(int value, int prefixMask, int bits) throws IOException { - // Write the raw value for a single byte value. - if (value < prefixMask) { - out.writeByte(bits | value); - return; - } - - // Write the mask to start a multibyte value. - out.writeByte(bits | prefixMask); - value -= prefixMask; - - // Write 7 bits at a time 'til we're done. - while (value >= 0x80) { - int b = value & 0x7f; - out.writeByte(b | 0x80); - value >>>= 7; - } - out.writeByte(value); + void writeByteString(ByteString data) throws IOException { + writeInt(data.size(), PREFIX_7_BITS, 0); + out.write(data); + } } - void writeByteString(ByteString data) throws IOException { - writeInt(data.size(), PREFIX_7_BITS, 0); - out.write(data); - } - } - - /** - * An HTTP/2 response cannot contain uppercase header characters and must - * be treated as malformed. - */ - private static ByteString checkLowercase(ByteString name) throws IOException { - for (int i = 0, length = name.size(); i < length; i++) { - byte c = name.getByte(i); - if (c >= 'A' && c <= 'Z') { - throw new IOException("PROTOCOL_ERROR response malformed: mixed case name: " + name.utf8()); - } + /** + * An HTTP/2 response cannot contain uppercase header characters and must + * be treated as malformed. + */ + private static ByteString checkLowercase(ByteString name) throws IOException { + for (int i = 0, length = name.size(); i < length; i++) { + byte c = name.getByte(i); + if (c >= 'A' && c <= 'Z') { + throw new IOException("PROTOCOL_ERROR response malformed: mixed case name: " + name.utf8()); + } + } + return name; } - return name; - } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java.ignore b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java.ignore deleted file mode 100644 index c2231970a..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Http20Draft13.java.ignore +++ /dev/null @@ -1,763 +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.koushikdutta.async.http.spdy.okhttp.internal.spdy; - -import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.BufferedSink; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; -import com.koushikdutta.async.http.spdy.okio.ByteString; -import com.koushikdutta.async.http.spdy.okio.Source; -import com.koushikdutta.async.http.spdy.okio.Timeout; - -import java.io.IOException; -import java.nio.ByteOrder; -import java.util.List; -import java.util.logging.Logger; - -import static com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Http20Draft13.FrameLogger.formatHeader; -import static com.koushikdutta.async.http.spdy.okio.ByteString.EMPTY; -import static java.lang.String.format; -import static java.util.logging.Level.FINE; - -/** - * Read and write HTTP/2 v13 frames. - *

http://tools.ietf.org/html/draft-ietf-httpbis-http2-13 - */ - -public final class Http20Draft13 implements Variant { - private static final Logger logger = Logger.getLogger(Http20Draft13.class.getName()); - - @Override public Protocol getProtocol() { - return Protocol.HTTP_2; - } - - private static final ByteString CONNECTION_PREFACE - = ByteString.encodeUtf8("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"); - - static final int MAX_FRAME_SIZE = 0x3fff; // 16383 - - static final byte TYPE_DATA = 0x0; - static final byte TYPE_HEADERS = 0x1; - static final byte TYPE_PRIORITY = 0x2; - static final byte TYPE_RST_STREAM = 0x3; - static final byte TYPE_SETTINGS = 0x4; - static final byte TYPE_PUSH_PROMISE = 0x5; - static final byte TYPE_PING = 0x6; - static final byte TYPE_GOAWAY = 0x7; - static final byte TYPE_WINDOW_UPDATE = 0x8; - static final byte TYPE_CONTINUATION = 0x9; - - static final byte FLAG_NONE = 0x0; - static final byte FLAG_ACK = 0x1; // Used for settings and ping. - static final byte FLAG_END_STREAM = 0x1; // Used for headers and data. - static final byte FLAG_END_SEGMENT = 0x2; - static final byte FLAG_END_HEADERS = 0x4; // Used for headers and continuation. - static final byte FLAG_END_PUSH_PROMISE = 0x4; - static final byte FLAG_PADDED = 0x8; // Used for headers and data. - static final byte FLAG_PRIORITY = 0x20; // Used for headers. - static final byte FLAG_COMPRESSED = 0x20; // Used for data. - - /** - * Creates a frame reader with max header table size of 4096 and data frame - * compression disabled. - */ - @Override public FrameReader newReader(BufferedSource source, boolean client) { - return new Reader(source, 4096, client); - } - - @Override public FrameWriter newWriter(BufferedSink sink, boolean client) { - return new Writer(sink, client); - } - - @Override public int maxFrameSize() { - return MAX_FRAME_SIZE; - } - - static final class Reader implements FrameReader { - private final BufferedSource source; - private final ContinuationSource continuation; - private final boolean client; - - // Visible for testing. - final HpackDraft08.Reader hpackReader; - - @Override - public int canProcessFrame(ByteBufferList bb) { - if (bb.remaining() < 4) - return 0; - bb.order(ByteOrder.BIG_ENDIAN); - int w1 = bb.peekInt(); - - short length = (short) ((w1 & 0x3fff0000) >> 16); // 14-bit unsigned == MAX_FRAME_SIZE - if (bb.remaining() < 8 + length) - return 0; - return 8 + length; - } - - Reader(BufferedSource source, int headerTableSize, boolean client) { - this.source = source; - this.client = client; - this.continuation = new ContinuationSource(this.source); - this.hpackReader = new HpackDraft08.Reader(headerTableSize, continuation); - } - - @Override public void readConnectionPreface() throws IOException { - if (client) return; // Nothing to read; servers doesn't send a connection preface! - ByteString connectionPreface = source.readByteString(CONNECTION_PREFACE.size()); - if (logger.isLoggable(FINE)) logger.fine(format("<< CONNECTION %s", connectionPreface.hex())); - if (!CONNECTION_PREFACE.equals(connectionPreface)) { - throw ioException("Expected a connection header but was %s", connectionPreface.utf8()); - } - } - - @Override public boolean nextFrame(Handler handler) throws IOException { - int w1; - int w2; - try { - w1 = source.readInt(); - w2 = source.readInt(); - } catch (IOException e) { - return false; // This might be a normal socket close. - } - - // boolean r = (w1 & 0xc0000000) != 0; // Reserved: Ignore first 2 bits. - short length = (short) ((w1 & 0x3fff0000) >> 16); // 14-bit unsigned == MAX_FRAME_SIZE - byte type = (byte) ((w1 & 0xff00) >> 8); - byte flags = (byte) (w1 & 0xff); - // boolean r = (w2 & 0x80000000) != 0; // Reserved: Ignore first bit. - int streamId = (w2 & 0x7fffffff); // 31-bit opaque identifier. - if (logger.isLoggable(FINE)) logger.fine(formatHeader(true, streamId, length, type, flags)); - - switch (type) { - case TYPE_DATA: - readData(handler, length, flags, streamId); - break; - - case TYPE_HEADERS: - readHeaders(handler, length, flags, streamId); - break; - - case TYPE_PRIORITY: - readPriority(handler, length, flags, streamId); - break; - - case TYPE_RST_STREAM: - readRstStream(handler, length, flags, streamId); - break; - - case TYPE_SETTINGS: - readSettings(handler, length, flags, streamId); - break; - - case TYPE_PUSH_PROMISE: - readPushPromise(handler, length, flags, streamId); - break; - - case TYPE_PING: - readPing(handler, length, flags, streamId); - break; - - case TYPE_GOAWAY: - readGoAway(handler, length, flags, streamId); - break; - - case TYPE_WINDOW_UPDATE: - readWindowUpdate(handler, length, flags, streamId); - break; - - default: - // Implementations MUST discard frames that have unknown or unsupported types. - source.skip(length); - } - return true; - } - - private void readHeaders(Handler handler, short length, byte flags, int streamId) - throws IOException { - if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0"); - - boolean endStream = (flags & FLAG_END_STREAM) != 0; - - short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; - - if ((flags & FLAG_PRIORITY) != 0) { - readPriority(handler, streamId); - length -= 5; // account for above read. - } - - length = lengthWithoutPadding(length, flags, padding); - - List

headerBlock = readHeaderBlock(length, padding, flags, streamId); - - handler.headers(false, endStream, streamId, -1, headerBlock, HeadersMode.HTTP_20_HEADERS); - } - - private List
readHeaderBlock(short length, short padding, byte flags, int streamId) - throws IOException { - continuation.length = continuation.left = length; - continuation.padding = padding; - continuation.flags = flags; - continuation.streamId = streamId; - - hpackReader.readHeaders(); - hpackReader.emitReferenceSet(); - // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20. - // http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3 - return hpackReader.getAndReset(); - } - - private void readData(Handler handler, short length, byte flags, int streamId) - throws IOException { - // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED - boolean inFinished = (flags & FLAG_END_STREAM) != 0; - boolean gzipped = (flags & FLAG_COMPRESSED) != 0; - if (gzipped) { - throw ioException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA"); - } - - short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; - length = lengthWithoutPadding(length, flags, padding); - - handler.data(inFinished, streamId, source, length); - source.skip(padding); - } - - private void readPriority(Handler handler, short length, byte flags, int streamId) - throws IOException { - if (length != 5) throw ioException("TYPE_PRIORITY length: %d != 5", length); - if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0"); - readPriority(handler, streamId); - } - - private void readPriority(Handler handler, int streamId) throws IOException { - int w1 = source.readInt(); - boolean exclusive = (w1 & 0x80000000) != 0; - int streamDependency = (w1 & 0x7fffffff); - int weight = (source.readByte() & 0xff) + 1; - handler.priority(streamId, streamDependency, weight, exclusive); - } - - private void readRstStream(Handler handler, short length, byte flags, 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 = source.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, short length, byte flags, int streamId) - throws IOException { - if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0"); - if ((flags & FLAG_ACK) != 0) { - if (length != 0) throw ioException("FRAME_SIZE_ERROR ack frame should be empty!"); - handler.ackSettings(); - return; - } - - if (length % 6 != 0) throw ioException("TYPE_SETTINGS length %% 6 != 0: %s", length); - Settings settings = new Settings(); - for (int i = 0; i < length; i += 6) { - short id = source.readShort(); - int value = source.readInt(); - - switch (id) { - case 1: // SETTINGS_HEADER_TABLE_SIZE - break; - case 2: // SETTINGS_ENABLE_PUSH - if (value != 0 && value != 1) { - throw ioException("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1"); - } - break; - case 3: // SETTINGS_MAX_CONCURRENT_STREAMS - id = 4; // Renumbered in draft 10. - break; - case 4: // SETTINGS_INITIAL_WINDOW_SIZE - id = 7; // Renumbered in draft 10. - if (value < 0) { - throw ioException("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1"); - } - break; - case 5: // SETTINGS_COMPRESS_DATA - break; - default: - throw ioException("PROTOCOL_ERROR invalid settings id: %s", id); - } - settings.set(id, 0, value); - } - handler.settings(false, settings); - if (settings.getHeaderTableSize() >= 0) { - hpackReader.maxHeaderTableByteCountSetting(settings.getHeaderTableSize()); - } - } - - private void readPushPromise(Handler handler, short length, byte flags, int streamId) - throws IOException { - if (streamId == 0) { - throw ioException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0"); - } - short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0; - int promisedStreamId = source.readInt() & 0x7fffffff; - length -= 4; // account for above read. - length = lengthWithoutPadding(length, flags, padding); - List
headerBlock = readHeaderBlock(length, padding, flags, streamId); - handler.pushPromise(streamId, promisedStreamId, headerBlock); - } - - private void readPing(Handler handler, short length, byte flags, 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 = source.readInt(); - int payload2 = source.readInt(); - boolean ack = (flags & FLAG_ACK) != 0; - handler.ping(ack, payload1, payload2); - } - - private void readGoAway(Handler handler, short length, byte flags, int streamId) - throws IOException { - if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length); - if (streamId != 0) throw ioException("TYPE_GOAWAY streamId != 0"); - int lastStreamId = source.readInt(); - int errorCodeInt = source.readInt(); - int opaqueDataLength = length - 8; - ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); - if (errorCode == null) { - throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt); - } - ByteString debugData = EMPTY; - if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection. - debugData = source.readByteString(opaqueDataLength); - } - handler.goAway(lastStreamId, errorCode, debugData); - } - - private void readWindowUpdate(Handler handler, short length, byte flags, int streamId) - throws IOException { - if (length != 4) throw ioException("TYPE_WINDOW_UPDATE length !=4: %s", length); - long increment = (source.readInt() & 0x7fffffffL); - if (increment == 0) throw ioException("windowSizeIncrement was 0", increment); - handler.windowUpdate(streamId, increment); - } - - @Override public void close() throws IOException { - source.close(); - } - } - - static final class Writer implements FrameWriter { - private final BufferedSink sink; - private final boolean client; - private final Buffer hpackBuffer; - private final HpackDraft08.Writer hpackWriter; - private boolean closed; - - Writer(BufferedSink sink, boolean client) { - this.sink = sink; - this.client = client; - this.hpackBuffer = new Buffer(); - this.hpackWriter = new HpackDraft08.Writer(hpackBuffer); - } - - @Override public synchronized void flush() throws IOException { - if (closed) throw new IOException("closed"); - sink.flush(); - } - - @Override public synchronized void ackSettings() throws IOException { - if (closed) throw new IOException("closed"); - int length = 0; - byte type = TYPE_SETTINGS; - byte flags = FLAG_ACK; - int streamId = 0; - frameHeader(streamId, length, type, flags); - sink.flush(); - } - - @Override public synchronized void connectionPreface() throws IOException { - if (closed) throw new IOException("closed"); - if (!client) return; // Nothing to write; servers don't send connection headers! - if (logger.isLoggable(FINE)) { - logger.fine(format(">> CONNECTION %s", CONNECTION_PREFACE.hex())); - } - sink.write(CONNECTION_PREFACE.toByteArray()); - sink.flush(); - } - - @Override public synchronized void synStream(boolean outFinished, boolean inFinished, - int streamId, int associatedStreamId, List
headerBlock) - throws IOException { - if (inFinished) throw new UnsupportedOperationException(); - if (closed) throw new IOException("closed"); - headers(outFinished, streamId, headerBlock); - } - - @Override public synchronized void synReply(boolean outFinished, int streamId, - List
headerBlock) throws IOException { - if (closed) throw new IOException("closed"); - headers(outFinished, streamId, headerBlock); - } - - @Override public synchronized void headers(int streamId, List
headerBlock) - throws IOException { - if (closed) throw new IOException("closed"); - headers(false, streamId, headerBlock); - } - - @Override public synchronized void pushPromise(int streamId, int promisedStreamId, - List
requestHeaders) throws IOException { - if (closed) throw new IOException("closed"); - if (hpackBuffer.size() != 0) throw new IllegalStateException(); - hpackWriter.writeHeaders(requestHeaders); - - long byteCount = hpackBuffer.size(); - int length = (int) Math.min(MAX_FRAME_SIZE - 4, byteCount); - byte type = TYPE_PUSH_PROMISE; - byte flags = byteCount == length ? FLAG_END_HEADERS : 0; - frameHeader(streamId, length + 4, type, flags); - sink.writeInt(promisedStreamId & 0x7fffffff); - sink.write(hpackBuffer, length); - - if (byteCount > length) writeContinuationFrames(streamId, byteCount - length); - } - - void headers(boolean outFinished, int streamId, List
headerBlock) throws IOException { - if (closed) throw new IOException("closed"); - if (hpackBuffer.size() != 0) throw new IllegalStateException(); - hpackWriter.writeHeaders(headerBlock); - - long byteCount = hpackBuffer.size(); - int length = (int) Math.min(MAX_FRAME_SIZE, byteCount); - byte type = TYPE_HEADERS; - byte flags = byteCount == length ? FLAG_END_HEADERS : 0; - if (outFinished) flags |= FLAG_END_STREAM; - frameHeader(streamId, length, type, flags); - sink.write(hpackBuffer, length); - - if (byteCount > length) writeContinuationFrames(streamId, byteCount - length); - } - - private void writeContinuationFrames(int streamId, long byteCount) throws IOException { - while (byteCount > 0) { - int length = (int) Math.min(MAX_FRAME_SIZE, byteCount); - byteCount -= length; - frameHeader(streamId, length, TYPE_CONTINUATION, byteCount == 0 ? FLAG_END_HEADERS : 0); - sink.write(hpackBuffer, length); - } - } - - @Override public synchronized void rstStream(int streamId, ErrorCode errorCode) - throws IOException { - if (closed) throw new IOException("closed"); - if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException(); - - int length = 4; - byte type = TYPE_RST_STREAM; - byte flags = FLAG_NONE; - frameHeader(streamId, length, type, flags); - sink.writeInt(errorCode.httpCode); - sink.flush(); - } - - @Override public synchronized void data(boolean outFinished, int streamId, Buffer source) - throws IOException { - data(outFinished, streamId, source, (int) source.size()); - } - - @Override public synchronized void data(boolean outFinished, int streamId, Buffer source, - int byteCount) throws IOException { - if (closed) throw new IOException("closed"); - byte flags = FLAG_NONE; - if (outFinished) flags |= FLAG_END_STREAM; - dataFrame(streamId, flags, source, byteCount); - } - - void dataFrame(int streamId, byte flags, Buffer buffer, int byteCount) throws IOException { - byte type = TYPE_DATA; - frameHeader(streamId, byteCount, type, flags); - if (byteCount > 0) { - sink.write(buffer, byteCount); - } - } - - @Override public synchronized void settings(Settings settings) throws IOException { - if (closed) throw new IOException("closed"); - int length = settings.size() * 6; - byte type = TYPE_SETTINGS; - byte flags = FLAG_NONE; - int streamId = 0; - frameHeader(streamId, length, type, flags); - for (int i = 0; i < Settings.COUNT; i++) { - if (!settings.isSet(i)) continue; - int id = i; - if (id == 4) id = 3; // SETTINGS_MAX_CONCURRENT_STREAMS renumbered. - else if (id == 7) id = 4; // SETTINGS_INITIAL_WINDOW_SIZE renumbered. - sink.writeShort(id); - sink.writeInt(settings.get(i)); - } - sink.flush(); - } - - @Override public synchronized void ping(boolean ack, int payload1, int payload2) - throws IOException { - if (closed) throw new IOException("closed"); - int length = 8; - byte type = TYPE_PING; - byte flags = ack ? FLAG_ACK : FLAG_NONE; - int streamId = 0; - frameHeader(streamId, length, type, flags); - sink.writeInt(payload1); - sink.writeInt(payload2); - sink.flush(); - } - - @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, - byte[] debugData) throws IOException { - if (closed) throw new IOException("closed"); - if (errorCode.httpCode == -1) throw illegalArgument("errorCode.httpCode == -1"); - int length = 8 + debugData.length; - byte type = TYPE_GOAWAY; - byte flags = FLAG_NONE; - int streamId = 0; - frameHeader(streamId, length, type, flags); - sink.writeInt(lastGoodStreamId); - sink.writeInt(errorCode.httpCode); - if (debugData.length > 0) { - sink.write(debugData); - } - sink.flush(); - } - - @Override public synchronized void windowUpdate(int streamId, long windowSizeIncrement) - throws IOException { - if (closed) throw new IOException("closed"); - if (windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL) { - throw illegalArgument("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: %s", - windowSizeIncrement); - } - int length = 4; - byte type = TYPE_WINDOW_UPDATE; - byte flags = FLAG_NONE; - frameHeader(streamId, length, type, flags); - sink.writeInt((int) windowSizeIncrement); - sink.flush(); - } - - @Override public synchronized void close() throws IOException { - closed = true; - sink.close(); - } - - void frameHeader(int streamId, int length, byte type, byte flags) throws IOException { - if (logger.isLoggable(FINE)) logger.fine(formatHeader(false, streamId, length, type, flags)); - if (length > MAX_FRAME_SIZE) { - throw illegalArgument("FRAME_SIZE_ERROR length > %d: %d", MAX_FRAME_SIZE, length); - } - if ((streamId & 0x80000000) != 0) throw illegalArgument("reserved bit set: %s", streamId); - sink.writeInt((length & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); - sink.writeInt(streamId & 0x7fffffff); - } - } - - private static IllegalArgumentException illegalArgument(String message, Object... args) { - throw new IllegalArgumentException(format(message, args)); - } - - private static IOException ioException(String message, Object... args) throws IOException { - throw new IOException(format(message, args)); - } - - /** - * Decompression of the header block occurs above the framing layer. This - * class lazily reads continuation frames as they are needed by {@link - * HpackDraft08.Reader#readHeaders()}. - */ - static final class ContinuationSource implements Source { - private final BufferedSource source; - - short length; - byte flags; - int streamId; - - short left; - short padding; - - public ContinuationSource(BufferedSource source) { - this.source = source; - } - - @Override public long read(Buffer sink, long byteCount) throws IOException { - while (left == 0) { - source.skip(padding); - padding = 0; - if ((flags & FLAG_END_HEADERS) != 0) return -1; - readContinuationHeader(); - // TODO: test case for empty continuation header? - } - - long read = source.read(sink, Math.min(byteCount, left)); - if (read == -1) return -1; - left -= read; - return read; - } - - @Override public Timeout timeout() { - return source.timeout(); - } - - @Override public void close() throws IOException { - } - - private void readContinuationHeader() throws IOException { - int previousStreamId = streamId; - int w1 = source.readInt(); - int w2 = source.readInt(); - length = left = (short) ((w1 & 0x3fff0000) >> 16); - byte type = (byte) ((w1 & 0xff00) >> 8); - flags = (byte) (w1 & 0xff); - if (logger.isLoggable(FINE)) logger.fine(formatHeader(true, streamId, length, type, flags)); - streamId = (w2 & 0x7fffffff); - if (type != TYPE_CONTINUATION) throw ioException("%s != TYPE_CONTINUATION", type); - if (streamId != previousStreamId) throw ioException("TYPE_CONTINUATION streamId changed"); - } - } - - private static short lengthWithoutPadding(short length, byte flags, short padding) - throws IOException { - if ((flags & FLAG_PADDED) != 0) length--; // Account for reading the padding length. - if (padding > length) { - throw ioException("PROTOCOL_ERROR padding %s > remaining length %s", padding, length); - } - return (short) (length - padding); - } - - /** - * Logs a human-readable representation of HTTP/2 frame headers. - * - *

The format is: - * - *

-   *   direction streamID length type flags
-   * 
- * Where direction is {@code <<} for inbound and {@code >>} for outbound. - * - *

For example, the following would indicate a HEAD request sent from - * the client. - *

-   * {@code
-   *   << 0x0000000f    12 HEADERS       END_HEADERS|END_STREAM
-   * }
-   * 
- */ - static final class FrameLogger { - - static String formatHeader(boolean inbound, int streamId, int length, byte type, byte flags) { - String formattedType = type < TYPES.length ? TYPES[type] : format("0x%02x", type); - String formattedFlags = formatFlags(type, flags); - return format("%s 0x%08x %5d %-13s %s", inbound ? "<<" : ">>", streamId, length, - formattedType, formattedFlags); - } - - /** - * Looks up valid string representing flags from the table. Invalid - * combinations are represented in binary. - */ - // Visible for testing. - static String formatFlags(byte type, byte flags) { - if (flags == 0) return ""; - switch (type) { // Special case types that have 0 or 1 flag. - case TYPE_SETTINGS: - case TYPE_PING: - return flags == FLAG_ACK ? "ACK" : BINARY[flags]; - case TYPE_PRIORITY: - case TYPE_RST_STREAM: - case TYPE_GOAWAY: - case TYPE_WINDOW_UPDATE: - return BINARY[flags]; - } - String result = flags < FLAGS.length ? FLAGS[flags] : BINARY[flags]; - // Special case types that have overlap flag values. - if (type == TYPE_PUSH_PROMISE && (flags & FLAG_END_PUSH_PROMISE) != 0) { - return result.replace("HEADERS", "PUSH_PROMISE"); // TODO: Avoid allocation. - } else if (type == TYPE_DATA && (flags & FLAG_COMPRESSED) != 0) { - return result.replace("PRIORITY", "COMPRESSED"); // TODO: Avoid allocation. - } - return result; - } - - /** Lookup table for valid frame types. */ - private static final String[] TYPES = new String[] { - "DATA", - "HEADERS", - "PRIORITY", - "RST_STREAM", - "SETTINGS", - "PUSH_PROMISE", - "PING", - "GOAWAY", - "WINDOW_UPDATE", - "CONTINUATION" - }; - - /** - * Lookup table for valid flags for DATA, HEADERS, CONTINUATION. Invalid - * combinations are represented in binary. - */ - private static final String[] FLAGS = new String[0x40]; // Highest bit flag is 0x20. - private static final String[] BINARY = new String[256]; - - static { - for (int i = 0; i < BINARY.length; i++) { - BINARY[i] = format("%8s", Integer.toBinaryString(i)).replace(' ', '0'); - } - - FLAGS[FLAG_NONE] = ""; - FLAGS[FLAG_END_STREAM] = "END_STREAM"; - FLAGS[FLAG_END_SEGMENT] = "END_SEGMENT"; - FLAGS[FLAG_END_STREAM | FLAG_END_SEGMENT] = "END_STREAM|END_SEGMENT"; - int[] prefixFlags = - new int[] {FLAG_END_STREAM, FLAG_END_SEGMENT, FLAG_END_SEGMENT | FLAG_END_STREAM}; - - FLAGS[FLAG_PADDED] = "PADDED"; - for (int prefixFlag : prefixFlags) { - FLAGS[prefixFlag | FLAG_PADDED] = FLAGS[prefixFlag] + "|PADDED"; - } - - FLAGS[FLAG_END_HEADERS] = "END_HEADERS"; // Same as END_PUSH_PROMISE. - FLAGS[FLAG_PRIORITY] = "PRIORITY"; // Same as FLAG_COMPRESSED. - FLAGS[FLAG_END_HEADERS | FLAG_PRIORITY] = "END_HEADERS|PRIORITY"; // Only valid on HEADERS. - int[] frameFlags = - new int[] {FLAG_END_HEADERS, FLAG_PRIORITY, FLAG_END_HEADERS | FLAG_PRIORITY}; - - for (int frameFlag : frameFlags) { - for (int prefixFlag : prefixFlags) { - FLAGS[prefixFlag | frameFlag] = FLAGS[prefixFlag] + '|' + FLAGS[frameFlag]; - FLAGS[prefixFlag | frameFlag | FLAG_PADDED] = - FLAGS[prefixFlag] + '|' + FLAGS[frameFlag] + "|PADDED"; - } - } - - for (int i = 0; i < FLAGS.length; i++) { // Fill in holes with binary representation. - if (FLAGS[i] == null) FLAGS[i] = BINARY[i]; - } - } - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java index 2868ff225..f4e7d4e26 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.io.Writer; import java.net.ProtocolException; import java.nio.ByteOrder; import java.util.List; @@ -248,9 +249,12 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { } }; + /* @Override public void readConnectionPreface() { } + */ + private void readSynStream(ByteBufferList source, int flags, int length) throws IOException { int w1 = source.getInt(); int w2 = source.getInt(); @@ -339,10 +343,6 @@ private void readSettings(ByteBufferList source, int flags, int length) throws I private static IOException ioException(String message, Object... args) throws IOException { throw new IOException(String.format(message, args)); } - - @Override - public void close() throws IOException { - } } /** From 87387af1b33156c1a20e3948f14f0e5abf3addf9 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 00:17:18 -0700 Subject: [PATCH 052/399] no more exceptions.[ --- .../com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java | 2 +- .../async/http/spdy/okhttp/internal/spdy/FrameReader.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index 05a546ed6..19878327a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -531,7 +531,7 @@ public void priority(int streamId, int streamDependency, int weight, boolean exc } @Override - public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) throws IOException { + public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) { throw new AssertionError("pushPromise"); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java index 087182126..05d2107de 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java @@ -117,8 +117,7 @@ void headers(boolean outFinished, boolean inFinished, int streamId, int associat * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, * {@code :authority}, and (@code :path}. */ - void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) - throws IOException; + void pushPromise(int streamId, int promisedStreamId, List
requestHeaders); /** * HTTP/2 only. Expresses that resources for the connection or a client- From 1a9a74309110a7c122abdaad0dd0ed0956283134 Mon Sep 17 00:00:00 2001 From: Steve Lhomme Date: Mon, 28 Jul 2014 09:55:36 +0200 Subject: [PATCH 053/399] use another hash function if MD5 is not available --- .../koushikdutta/async/util/FileCache.java | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/util/FileCache.java b/AndroidAsync/src/com/koushikdutta/async/util/FileCache.java index 3ce4b97af..d1171083e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/util/FileCache.java +++ b/AndroidAsync/src/com/koushikdutta/async/util/FileCache.java @@ -6,6 +6,8 @@ import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.Security; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -41,17 +43,42 @@ public void close() { } } + private static String hashAlgorithm = "MD5"; + + private static MessageDigest findAlternativeMessageDigest() { + if ("MD5".equals(hashAlgorithm)) { + for (Provider provider : Security.getProviders()) { + for (Provider.Service service : provider.getServices()) { + hashAlgorithm = service.getAlgorithm(); + try { + MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm); + if (messageDigest != null) + return messageDigest; + } catch (NoSuchAlgorithmException ignored) { + } + } + } + } + return null; + } + public static String toKeyString(Object... parts) { - try { - MessageDigest messageDigest = MessageDigest.getInstance("MD5"); - for (Object part: parts) { - messageDigest.update(part.toString().getBytes()); + MessageDigest messageDigest; + synchronized (FileCache.class) { + try { + messageDigest = MessageDigest.getInstance(hashAlgorithm); + } catch (NoSuchAlgorithmException e) { + messageDigest = findAlternativeMessageDigest(); + if (null == messageDigest) + throw new RuntimeException(e); } - byte[] md5bytes = messageDigest.digest(); - return new BigInteger(1, md5bytes).toString(16); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); } + + for (Object part : parts) { + messageDigest.update(part.toString().getBytes()); + } + byte[] md5bytes = messageDigest.digest(); + return new BigInteger(1, md5bytes).toString(16); } boolean loadAsync; From 8f5a76fef616565541dacbae0a9d7841d9a33e98 Mon Sep 17 00:00:00 2001 From: Steve Lhomme Date: Mon, 28 Jul 2014 10:06:32 +0200 Subject: [PATCH 054/399] Fix a rare NPE crash in Android 2.x See http://crashes.to/s/5ee30776a5d --- AndroidAsync/src/com/koushikdutta/async/AsyncServer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java index 6d5e5575e..450dc5b60 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java @@ -727,6 +727,9 @@ private static void runLoop(final AsyncServer server, final SelectorWrapper sele } } } + catch (NullPointerException e) { + throw new AsyncSelectorException(e); + } catch (IOException e) { throw new AsyncSelectorException(e); } From c5d92d36e9613a47e54a8591484900277a2137fb Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 02:16:24 -0700 Subject: [PATCH 055/399] New writer seems to work? --- .../koushikdutta/async/ByteBufferList.java | 23 +- .../async/http/spdy/AsyncSpdyConnection.java | 14 +- .../okhttp/internal/spdy/FrameWriter.java | 7 +- .../okhttp/internal/spdy/HpackDraft08.java | 48 +++-- .../http/spdy/okhttp/internal/spdy/Spdy3.java | 202 ++++++++++-------- .../spdy/okhttp/internal/spdy/Variant.java | 4 +- 6 files changed, 158 insertions(+), 140 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java index 84c33074b..b42a58a19 100644 --- a/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java +++ b/AndroidAsync/src/com/koushikdutta/async/ByteBufferList.java @@ -41,9 +41,16 @@ public ByteBufferList(byte[] buf) { add(b); } - public void addAll(ByteBuffer... bb) { + public ByteBufferList addAll(ByteBuffer... bb) { for (ByteBuffer b: bb) add(b); + return this; + } + + public ByteBufferList addAll(ByteBufferList... bb) { + for (ByteBufferList b: bb) + b.get(this); + return this; } public byte[] getBytes(int length) { @@ -268,12 +275,17 @@ public void trim() { // this clears out buffers that are empty in the beginning of the list read(0); } - - public void add(ByteBuffer b) { + + public ByteBufferList add(ByteBufferList b) { + b.get(this); + return this; + } + + public ByteBufferList add(ByteBuffer b) { if (b.remaining() <= 0) { // System.out.println("reclaiming remaining: " + b.remaining()); reclaim(b); - return; + return this; } addRemaining(b.remaining()); // see if we can fit the entirety of the buffer into the end @@ -289,11 +301,12 @@ public void add(ByteBuffer b) { last.reset(); reclaim(b); trim(); - return; + return this; } } mBuffers.add(b); trim(); + return this; } public void addFirst(ByteBuffer b) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index 19878327a..a961cbf3d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -2,7 +2,6 @@ import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; -import com.koushikdutta.async.BufferedDataEmitter; import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.Util; @@ -23,7 +22,6 @@ import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Variant; import com.koushikdutta.async.http.spdy.okio.BufferedSink; import com.koushikdutta.async.http.spdy.okio.ByteString; -import com.koushikdutta.async.http.spdy.okio.Okio; import java.io.IOException; import java.util.Hashtable; @@ -97,10 +95,6 @@ private SpdySocket newStream(int associatedStreamId, List
requestHeaders writer.pushPromise(associatedStreamId, streamId, requestHeaders); } - if (!out) { - writer.flush(); - } - return socket; } catch (IOException e) { @@ -154,12 +148,6 @@ void updateWindowRead(int length) { public SpdySocket(int id, boolean outFinished, boolean inFinished, List
headerBlock) { this.id = id; - try { - writer.windowUpdate(id, DEFAULT_INITIAL_WINDOW_SIZE); - } - catch (IOException e) { - throw new AssertionError(e); - } } public boolean isLocallyInitiated() { @@ -285,7 +273,7 @@ else if (protocol == Protocol.HTTP_2) { variant = new Http20Draft13(); } reader = variant.newReader(socket, this, true); - writer = variant.newWriter(bufferedSink = Okio.buffer(sink), true); + writer = variant.newWriter(bufferedSocket, true); boolean client = true; nextStreamId = client ? 1 : 2; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java index f8781767c..5bbf060c2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java @@ -16,7 +16,7 @@ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; -import com.koushikdutta.async.http.spdy.okio.Buffer; +import com.koushikdutta.async.ByteBufferList; import java.io.Closeable; import java.io.IOException; @@ -47,7 +47,6 @@ void pushPromise(int streamId, int promisedStreamId, List
requestHeaders throws IOException; /** SPDY/3 only. */ - void flush() throws IOException; void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, List
headerBlock) throws IOException; void synReply(boolean outFinished, int streamId, List
headerBlock) @@ -61,9 +60,7 @@ void synReply(boolean outFinished, int streamId, List
headerBlock) * * @param source the buffer to draw bytes from. May be null if byteCount is 0. */ - void data(boolean outFinished, int streamId, Buffer source, int byteCount) throws IOException; - - void data(boolean outFinished, int streamId, Buffer source) throws IOException; + void data(boolean outFinished, int streamId, ByteBufferList source) throws IOException; /** Write okhttp's settings to the peer. */ void settings(Settings okHttpSettings) throws IOException; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java index f578cb225..b58d02958 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java @@ -17,13 +17,10 @@ import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.http.spdy.okhttp.internal.BitArray; -import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; import com.koushikdutta.async.http.spdy.okio.ByteString; -import com.koushikdutta.async.http.spdy.okio.Okio; -import com.koushikdutta.async.http.spdy.okio.Source; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -434,57 +431,64 @@ private static Map nameToFirstIndex() { } static final class Writer { - private final Buffer out; - - Writer(Buffer out) { - this.out = out; + Writer() { } /** * This does not use "never indexed" semantics for sensitive headers. */ // https://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.3.3 - void writeHeaders(List
headerBlock) throws IOException { + ByteBufferList writeHeaders(List
headerBlock) throws IOException { + ByteBufferList out = new ByteBufferList(); // TODO: implement index tracking + ByteBuffer current = ByteBufferList.obtain(8192); for (int i = 0, size = headerBlock.size(); i < size; i++) { + if (current.remaining() < current.capacity() / 2) { + current.flip(); + out.add(current); + current = ByteBufferList.obtain(current.capacity() * 2); + } ByteString name = headerBlock.get(i).name.toAsciiLowercase(); Integer staticIndex = NAME_TO_FIRST_INDEX.get(name); if (staticIndex != null) { // Literal Header Field without Indexing - Indexed Name. - writeInt(staticIndex + 1, PREFIX_4_BITS, 0); - writeByteString(headerBlock.get(i).value); + writeInt(current, staticIndex + 1, PREFIX_4_BITS, 0); + writeByteString(current, headerBlock.get(i).value); } else { - out.writeByte(0x00); // Literal Header without Indexing - New Name. - writeByteString(name); - writeByteString(headerBlock.get(i).value); + current.put((byte) 0x00); // Literal Header without Indexing - New Name. + writeByteString(current, name); + writeByteString(current, headerBlock.get(i).value); } } + + out.add(current); + return out; } // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08#section-4.1.1 - void writeInt(int value, int prefixMask, int bits) throws IOException { + void writeInt(ByteBuffer out, int value, int prefixMask, int bits) throws IOException { // Write the raw value for a single byte value. if (value < prefixMask) { - out.writeByte(bits | value); + out.put((byte) (bits | value)); return; } // Write the mask to start a multibyte value. - out.writeByte(bits | prefixMask); + out.put((byte)(bits | prefixMask)); value -= prefixMask; // Write 7 bits at a time 'til we're done. while (value >= 0x80) { int b = value & 0x7f; - out.writeByte(b | 0x80); + out.put((byte) (b | 0x80)); value >>>= 7; } - out.writeByte(value); + out.put((byte) value); } - void writeByteString(ByteString data) throws IOException { - writeInt(data.size(), PREFIX_7_BITS, 0); - out.write(data); + void writeByteString(ByteBuffer out, ByteString data) throws IOException { + writeInt(out, data.size(), PREFIX_7_BITS, 0); + out.put(data.toByteArray()); } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java index f4e7d4e26..3db622365 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java @@ -15,23 +15,19 @@ */ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataEmitterReader; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.http.spdy.okhttp.internal.Util; -import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.BufferedSink; import com.koushikdutta.async.http.spdy.okio.ByteString; -import com.koushikdutta.async.http.spdy.okio.DeflaterSink; -import com.koushikdutta.async.http.spdy.okio.Okio; import com.koushikdutta.async.util.Charsets; import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.io.Writer; import java.net.ProtocolException; +import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.List; import java.util.zip.Deflater; @@ -111,7 +107,7 @@ public FrameReader newReader(DataEmitter source, FrameReader.Handler handler, bo } @Override - public FrameWriter newWriter(BufferedSink sink, boolean client) { + public FrameWriter newWriter(BufferedDataSink sink, boolean client) { return new Writer(sink, client); } @@ -349,20 +345,17 @@ private static IOException ioException(String message, Object... args) throws IO * Write spdy/3 frames. */ static final class Writer implements FrameWriter { - private final BufferedSink sink; - private final Buffer headerBlockBuffer; - private final BufferedSink headerBlockOut; + private final BufferedDataSink sink; private final boolean client; private boolean closed; + private ByteBufferList frameHeader = new ByteBufferList(); + private final Deflater deflater = new Deflater(); - Writer(BufferedSink sink, boolean client) { + Writer(BufferedDataSink sink, boolean client) { this.sink = sink; this.client = client; - Deflater deflater = new Deflater(); deflater.setDictionary(DICTIONARY); - headerBlockBuffer = new Buffer(); - headerBlockOut = Okio.buffer(new DeflaterSink(headerBlockBuffer, deflater)); } @Override @@ -381,61 +374,59 @@ public synchronized void connectionPreface() { // Do nothing: no connection preface for SPDY/3. } - @Override - public synchronized void flush() throws IOException { - if (closed) throw new IOException("closed"); - sink.flush(); - } - @Override public synchronized void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, List
headerBlock) throws IOException { if (closed) throw new IOException("closed"); - writeNameValueBlockToBuffer(headerBlock); - int length = (int) (10 + headerBlockBuffer.size()); + ByteBufferList headerBlockBuffer = writeNameValueBlockToBuffer(headerBlock); + int length = (int) (10 + headerBlockBuffer.remaining()); int type = TYPE_SYN_STREAM; int flags = (outFinished ? FLAG_FIN : 0) | (inFinished ? FLAG_UNIDIRECTIONAL : 0); int unused = 0; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId & 0x7fffffff); - sink.writeInt(associatedStreamId & 0x7fffffff); - sink.writeShort((unused & 0x7) << 13 | (unused & 0x1f) << 8 | (unused & 0xff)); - sink.writeAll(headerBlockBuffer); - sink.flush(); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.putInt((flags & 0xff) << 24 | length & 0xffffff); + sink.putInt(streamId & 0x7fffffff); + sink.putInt(associatedStreamId & 0x7fffffff); + sink.putShort((short) ((unused & 0x7) << 13 | (unused & 0x1f) << 8 | (unused & 0xff))); + sink.flip(); + this.sink.write(frameHeader.add(sink).add(headerBlockBuffer)); } @Override public synchronized void synReply(boolean outFinished, int streamId, List
headerBlock) throws IOException { if (closed) throw new IOException("closed"); - writeNameValueBlockToBuffer(headerBlock); + ByteBufferList headerBlockBuffer = writeNameValueBlockToBuffer(headerBlock); int type = TYPE_SYN_REPLY; int flags = (outFinished ? FLAG_FIN : 0); - int length = (int) (headerBlockBuffer.size() + 4); - - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId & 0x7fffffff); - sink.writeAll(headerBlockBuffer); - sink.flush(); + int length = (int) (headerBlockBuffer.remaining() + 4); + + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.putInt((flags & 0xff) << 24 | length & 0xffffff); + sink.putInt(streamId & 0x7fffffff); + sink.flip(); + this.sink.write(frameHeader.add(sink).add(headerBlockBuffer)); } @Override public synchronized void headers(int streamId, List
headerBlock) throws IOException { if (closed) throw new IOException("closed"); - writeNameValueBlockToBuffer(headerBlock); + ByteBufferList headerBlockBuffer = writeNameValueBlockToBuffer(headerBlock); int flags = 0; int type = TYPE_HEADERS; - int length = (int) (headerBlockBuffer.size() + 4); - - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId & 0x7fffffff); - sink.writeAll(headerBlockBuffer); + int length = (int) (headerBlockBuffer.remaining() + 4); + + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.putInt((flags & 0xff) << 24 | length & 0xffffff); + sink.putInt(streamId & 0x7fffffff); + sink.flip(); + this.sink.write(frameHeader.add(sink).add(headerBlockBuffer)); } @Override @@ -446,51 +437,69 @@ public synchronized void rstStream(int streamId, ErrorCode errorCode) int flags = 0; int type = TYPE_RST_STREAM; int length = 8; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId & 0x7fffffff); - sink.writeInt(errorCode.spdyRstCode); - sink.flush(); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.putInt((flags & 0xff) << 24 | length & 0xffffff); + sink.putInt(streamId & 0x7fffffff); + sink.putInt(errorCode.spdyRstCode); + sink.flip(); + this.sink.write(frameHeader.addAll(sink)); } @Override - public synchronized void data(boolean outFinished, int streamId, Buffer source) - throws IOException { - data(outFinished, streamId, source, (int) source.size()); - } - - @Override - public synchronized void data(boolean outFinished, int streamId, Buffer source, - int byteCount) throws IOException { + public synchronized void data(boolean outFinished, int streamId, ByteBufferList source) throws IOException { int flags = (outFinished ? FLAG_FIN : 0); - sendDataFrame(streamId, flags, source, byteCount); + sendDataFrame(streamId, flags, source); } - void sendDataFrame(int streamId, int flags, Buffer buffer, int byteCount) + ByteBufferList dataList = new ByteBufferList(); + void sendDataFrame(int streamId, int flags, ByteBufferList buffer) throws IOException { if (closed) throw new IOException("closed"); + int byteCount = buffer.remaining(); if (byteCount > 0xffffffL) { throw new IllegalArgumentException("FRAME_TOO_LARGE max size is 16Mib: " + byteCount); } - sink.writeInt(streamId & 0x7fffffff); - sink.writeInt((flags & 0xff) << 24 | byteCount & 0xffffff); - if (byteCount > 0) { - sink.write(buffer, byteCount); - } + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(streamId & 0x7fffffff); + sink.putInt((flags & 0xff) << 24 | byteCount & 0xffffff); + sink.flip(); + dataList.add(sink).add(buffer); + this.sink.write(dataList); } - private void writeNameValueBlockToBuffer(List
headerBlock) throws IOException { - if (headerBlockBuffer.size() != 0) throw new IllegalStateException(); - headerBlockOut.writeInt(headerBlock.size()); + ByteBufferList headerBlockList = new ByteBufferList(); + private ByteBufferList writeNameValueBlockToBuffer(List
headerBlock) throws IOException { + if (headerBlockList.hasRemaining()) throw new IllegalStateException(); + ByteBuffer headerBlockOut = ByteBufferList.obtain(8192).order(ByteOrder.BIG_ENDIAN); + headerBlockOut.putInt(headerBlock.size()); for (int i = 0, size = headerBlock.size(); i < size; i++) { ByteString name = headerBlock.get(i).name; - headerBlockOut.writeInt(name.size()); - headerBlockOut.write(name); + headerBlockOut.putInt(name.size()); + headerBlockOut.put(name.toByteArray()); ByteString value = headerBlock.get(i).value; - headerBlockOut.writeInt(value.size()); - headerBlockOut.write(value); + headerBlockOut.putInt(value.size()); + headerBlockOut.put(value.toByteArray()); + if (headerBlockOut.remaining() < headerBlockOut.capacity() / 2) { + ByteBuffer newOut = ByteBufferList.obtain(headerBlockOut.capacity() * 2).order(ByteOrder.BIG_ENDIAN); + headerBlockOut.flip(); + newOut.put(headerBlockOut); + ByteBufferList.reclaim(headerBlockOut); + headerBlockOut = newOut; + } } - headerBlockOut.flush(); + + headerBlockOut.flip(); + deflater.setInput(headerBlockOut.array(), 0, headerBlockOut.remaining()); + while (!deflater.needsInput()) { + ByteBuffer deflated = ByteBufferList.obtain(headerBlockOut.capacity()).order(ByteOrder.BIG_ENDIAN); + int read = deflater.deflate(deflated.array(), 0, deflated.capacity(), Deflater.SYNC_FLUSH); + deflated.limit(read); + headerBlockList.add(deflated); + } + ByteBufferList.reclaim(headerBlockOut); + + return headerBlockList; } @Override @@ -500,16 +509,18 @@ public synchronized void settings(Settings settings) throws IOException { int flags = 0; int size = settings.size(); int length = 4 + size * 8; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(size); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.putInt((flags & 0xff) << 24 | length & 0xffffff); + sink.putInt(size); for (int i = 0; i <= Settings.COUNT; i++) { if (!settings.isSet(i)) continue; int settingsFlags = settings.flags(i); - sink.writeInt((settingsFlags & 0xff) << 24 | (i & 0xffffff)); - sink.writeInt(settings.get(i)); + sink.putInt((settingsFlags & 0xff) << 24 | (i & 0xffffff)); + sink.putInt(settings.get(i)); } - sink.flush(); + sink.flip(); + this.sink.write(frameHeader.addAll(sink)); } @Override @@ -521,10 +532,12 @@ public synchronized void ping(boolean reply, int payload1, int payload2) int type = TYPE_PING; int flags = 0; int length = 4; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(payload1); - sink.flush(); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.putInt((flags & 0xff) << 24 | length & 0xffffff); + sink.putInt(payload1); + sink.flip(); + this.sink.write(frameHeader.addAll(sink)); } @Override @@ -537,11 +550,13 @@ public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, int type = TYPE_GOAWAY; int flags = 0; int length = 8; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(lastGoodStreamId); - sink.writeInt(errorCode.spdyGoAwayCode); - sink.flush(); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.putInt((flags & 0xff) << 24 | length & 0xffffff); + sink.putInt(lastGoodStreamId); + sink.putInt(errorCode.spdyGoAwayCode); + sink.flip(); + this.sink.write(frameHeader.addAll(sink)); } @Override @@ -555,17 +570,18 @@ public synchronized void windowUpdate(int streamId, long increment) int type = TYPE_WINDOW_UPDATE; int flags = 0; int length = 8; - sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); - sink.writeInt((flags & 0xff) << 24 | length & 0xffffff); - sink.writeInt(streamId); - sink.writeInt((int) increment); - sink.flush(); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff); + sink.putInt((flags & 0xff) << 24 | length & 0xffffff); + sink.putInt(streamId); + sink.putInt((int) increment); + sink.flip(); + this.sink.write(frameHeader.addAll(sink)); } @Override public synchronized void close() throws IOException { closed = true; - Util.closeAll(sink, headerBlockOut); } } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java index eeb7c157e..00b12ffaf 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java @@ -16,9 +16,9 @@ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.http.spdy.okio.BufferedSink; /** A version and dialect of the framed socket protocol. */ public interface Variant { @@ -34,7 +34,7 @@ public interface Variant { /** * @param client true if this is the HTTP client's writer, writing frames to a server. */ - FrameWriter newWriter(BufferedSink sink, boolean client); + FrameWriter newWriter(BufferedDataSink sink, boolean client); int maxFrameSize(); } From bbc47ab4b3b093f09d96a6dee1a0b460e2f903ea Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 02:20:11 -0700 Subject: [PATCH 056/399] remove cruft --- .../async/http/spdy/AsyncSpdyConnection.java | 13 +- .../async/http/spdy/ByteBufferListSink.java | 52 ------ .../async/http/spdy/ByteBufferListSource.java | 40 ----- .../async/http/spdy/SpdyMiddleware.java | 2 - .../async/http/spdy/okio/DeflaterSink.java | 150 ------------------ .../http/spdy/okio/ForwardingSource.java | 49 ------ .../async/http/spdy/okio/InflaterSource.java | 123 -------------- 7 files changed, 1 insertion(+), 428 deletions(-) delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSink.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/DeflaterSink.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ForwardingSource.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index a961cbf3d..5c5f95c35 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -40,21 +40,10 @@ public class AsyncSpdyConnection implements FrameReader.Handler { FrameReader reader; FrameWriter writer; Variant variant; - ByteBufferListSink sink = new ByteBufferListSink() { - @Override - public void flush() throws IOException { - AsyncSpdyConnection.this.flush(); - } - }; - BufferedSink bufferedSink; Hashtable sockets = new Hashtable(); Protocol protocol; boolean client = true; - public void flush() { - bufferedSocket.write(sink); - } - /** * Returns a new locally-initiated stream. * @@ -123,7 +112,7 @@ public class SpdySocket implements AsyncSocket { CompletedCallback closedCallback; CompletedCallback endCallback; DataCallback dataCallback; - ByteBufferListSink pending = new ByteBufferListSink(); + ByteBufferList pending = new ByteBufferList(); SimpleFuture> headers = new SimpleFuture>(); boolean isOpen = true; int totalWindowRead; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSink.java deleted file mode 100644 index aa9da9a15..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSink.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.koushikdutta.async.http.spdy; - -import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.Segment; -import com.koushikdutta.async.http.spdy.okio.SegmentPool; -import com.koushikdutta.async.http.spdy.okio.Sink; -import com.koushikdutta.async.http.spdy.okio.Timeout; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Created by koush on 7/25/14. - */ -public class ByteBufferListSink extends ByteBufferList implements Sink { - @Override - public void write(Buffer source, long byteCount) throws IOException { - Segment s = source.head; - while (byteCount > 0) { - int toCopy = (int) Math.min(byteCount, s.limit - s.pos); - ByteBuffer b = obtain(toCopy); - b.put(s.data, s.pos, toCopy); - b.flip(); - add(b); - - s.pos += toCopy; - source.size -= toCopy; - byteCount -= toCopy; - - if (s.pos == s.limit) { - Segment toRecycle = s; - source.head = s = toRecycle.pop(); - SegmentPool.getInstance().recycle(toRecycle); - } - } - } - - @Override - public void flush() throws IOException { - } - - @Override - public Timeout timeout() { - return Timeout.NONE; - } - - @Override - public void close() throws IOException { - recycle(); - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java deleted file mode 100644 index 13803b3be..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteBufferListSource.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.koushikdutta.async.http.spdy; - -import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okio.Buffer; -import com.koushikdutta.async.http.spdy.okio.Source; -import com.koushikdutta.async.http.spdy.okio.Timeout; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Created by koush on 7/25/14. - */ -public class ByteBufferListSource extends ByteBufferList implements Source { - @Override - public long read(Buffer sink, long byteCount) throws IOException { - if (!hasRemaining()) - throw new AssertionError("empty!"); - int total = 0; - while (total < byteCount && hasRemaining()) { - ByteBuffer b = remove(); - int toRead = (int)Math.min(byteCount - total, b.remaining()); - total += toRead; - sink.write(b.array(), b.arrayOffset() + b.position(), toRead); - b.position(b.position() + toRead); - addFirst(b); - } - return total; - } - - @Override - public Timeout timeout() { - return Timeout.NONE; - } - - @Override - public void close() throws IOException { - recycle(); - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index 0e09fdd79..b97f261be 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -83,7 +83,6 @@ public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { } final AsyncSpdyConnection connection = new AsyncSpdyConnection(socket, Protocol.get(protoString)); connection.sendConnectionPreface(); - connection.flush(); connections.put(data.request.getUri().getHost(), connection); @@ -123,7 +122,6 @@ private void newSocket(GetSocketData data, final AsyncSpdyConnection connection, } AsyncSpdyConnection.SpdySocket spdy = connection.newStream(headers, false, true); - connection.flush(); callback.onConnectCompleted(null, spdy); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/DeflaterSink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/DeflaterSink.java deleted file mode 100644 index 960ee80c3..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/DeflaterSink.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.IOException; -import java.util.zip.Deflater; - -import static com.koushikdutta.async.http.spdy.okio.Util.checkOffsetAndCount; - -/** - * A sink that uses DEFLATE to - * compress data written to another source. - * - *

Sync flush

- * Aggressive flushing of this stream may result in reduced compression. Each - * call to {@link #flush} immediately compresses all currently-buffered data; - * this early compression may be less effective than compression performed - * without flushing. - * - *

This is equivalent to using {@link java.util.zip.Deflater} with the sync flush option. - * This class does not offer any partial flush mechanism. For best performance, - * only call {@link #flush} when application behavior requires it. - */ -public final class DeflaterSink implements Sink { - private final BufferedSink sink; - private final Deflater deflater; - private boolean closed; - - public DeflaterSink(Sink sink, Deflater deflater) { - this(Okio.buffer(sink), deflater); - } - - /** - * This package-private constructor shares a buffer with its trusted caller. - * In general we can't share a BufferedSource because the deflater holds input - * bytes until they are inflated. - */ - DeflaterSink(BufferedSink sink, Deflater deflater) { - if (sink == null) throw new IllegalArgumentException("source == null"); - if (deflater == null) throw new IllegalArgumentException("inflater == null"); - this.sink = sink; - this.deflater = deflater; - } - - @Override public void write(Buffer source, long byteCount) - throws IOException { - checkOffsetAndCount(source.size, 0, byteCount); - while (byteCount > 0) { - // Share bytes from the head segment of 'source' with the deflater. - Segment head = source.head; - int toDeflate = (int) Math.min(byteCount, head.limit - head.pos); - deflater.setInput(head.data, head.pos, toDeflate); - - // Deflate those bytes into sink. - deflate(false); - - // Mark those bytes as read. - source.size -= toDeflate; - head.pos += toDeflate; - if (head.pos == head.limit) { - source.head = head.pop(); - SegmentPool.getInstance().recycle(head); - } - - byteCount -= toDeflate; - } - } - - private void deflate(boolean syncFlush) throws IOException { - Buffer buffer = sink.buffer(); - while (true) { - Segment s = buffer.writableSegment(1); - - // The 4-parameter overload of deflate() doesn't exist in the RI until - // Java 1.7, and is public (although with @hide) on Android since 2.3. - // The @hide tag means that this code won't compile against the Android - // 2.3 SDK, but it will run fine there. - int deflated = syncFlush - ? deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit, Deflater.SYNC_FLUSH) - : deflater.deflate(s.data, s.limit, Segment.SIZE - s.limit); - - if (deflated > 0) { - s.limit += deflated; - buffer.size += deflated; - sink.emitCompleteSegments(); - } else if (deflater.needsInput()) { - return; - } - } - } - - @Override public void flush() throws IOException { - deflate(true); - sink.flush(); - } - - void finishDeflate() throws IOException { - deflater.finish(); - deflate(false); - } - - @Override public void close() throws IOException { - if (closed) return; - - // Emit deflated data to the underlying sink. If this fails, we still need - // to close the deflater and the sink; otherwise we risk leaking resources. - Throwable thrown = null; - try { - finishDeflate(); - } catch (Throwable e) { - thrown = e; - } - - try { - deflater.end(); - } catch (Throwable e) { - if (thrown == null) thrown = e; - } - - try { - sink.close(); - } catch (Throwable e) { - if (thrown == null) thrown = e; - } - closed = true; - - if (thrown != null) Util.sneakyRethrow(thrown); - } - - @Override public Timeout timeout() { - return sink.timeout(); - } - - @Override public String toString() { - return "DeflaterSink(" + sink + ")"; - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ForwardingSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ForwardingSource.java deleted file mode 100644 index 5e48dfd54..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ForwardingSource.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.IOException; - -/** A {@link Source} which forwards calls to another. Useful for subclassing. */ -public abstract class ForwardingSource implements Source { - private final Source delegate; - - public ForwardingSource(Source delegate) { - if (delegate == null) throw new IllegalArgumentException("delegate == null"); - this.delegate = delegate; - } - - /** {@link Source} to which this instance is delegating. */ - public final Source delegate() { - return delegate; - } - - @Override public long read(Buffer sink, long byteCount) throws IOException { - return delegate.read(sink, byteCount); - } - - @Override public Timeout timeout() { - return delegate.timeout(); - } - - @Override public void close() throws IOException { - delegate.close(); - } - - @Override public String toString() { - return getClass().getSimpleName() + "(" + delegate.toString() + ")"; - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java deleted file mode 100644 index 76f7cc031..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/InflaterSource.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.EOFException; -import java.io.IOException; -import java.util.zip.DataFormatException; -import java.util.zip.Inflater; - -/** - * A source that uses DEFLATE - * to decompress data read from another source. - */ -public final class InflaterSource implements Source { - private final BufferedSource source; - private final Inflater inflater; - - /** - * When we call Inflater.setInput(), the inflater keeps our byte array until - * it needs input again. This tracks how many bytes the inflater is currently - * holding on to. - */ - private int bufferBytesHeldByInflater; - private boolean closed; - - public InflaterSource(Source source, Inflater inflater) { - this(Okio.buffer(source), inflater); - } - - /** - * This package-private constructor shares a buffer with its trusted caller. - * In general we can't share a BufferedSource because the inflater holds input - * bytes until they are inflated. - */ - InflaterSource(BufferedSource source, Inflater inflater) { - if (source == null) throw new IllegalArgumentException("source == null"); - if (inflater == null) throw new IllegalArgumentException("inflater == null"); - this.source = source; - this.inflater = inflater; - } - - @Override public long read( - Buffer sink, long byteCount) throws IOException { - if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); - if (closed) throw new IllegalStateException("closed"); - if (byteCount == 0) return 0; - - while (true) { - boolean sourceExhausted = refill(); - - // Decompress the inflater's compressed data into the sink. - try { - Segment tail = sink.writableSegment(1); - int bytesInflated = inflater.inflate(tail.data, tail.limit, Segment.SIZE - tail.limit); - if (bytesInflated > 0) { - tail.limit += bytesInflated; - sink.size += bytesInflated; - return bytesInflated; - } - if (inflater.finished() || inflater.needsDictionary()) { - releaseInflatedBytes(); - return -1; - } - if (sourceExhausted) throw new EOFException("source exhausted prematurely"); - } catch (DataFormatException e) { - throw new IOException(e); - } - } - } - - /** - * Refills the inflater with compressed data if it needs input. (And only if - * it needs input). Returns true if the inflater required input but the source - * was exhausted. - */ - public boolean refill() throws IOException { - if (!inflater.needsInput()) return false; - - releaseInflatedBytes(); - if (inflater.getRemaining() != 0) throw new IllegalStateException("?"); // TODO: possible? - - // If there are compressed bytes in the source, assign them to the inflater. - if (source.exhausted()) return true; - - // Assign buffer bytes to the inflater. - Segment head = source.buffer().head; - bufferBytesHeldByInflater = head.limit - head.pos; - inflater.setInput(head.data, head.pos, bufferBytesHeldByInflater); - return false; - } - - /** When the inflater has processed compressed data, remove it from the buffer. */ - private void releaseInflatedBytes() throws IOException { - if (bufferBytesHeldByInflater == 0) return; - int toRelease = bufferBytesHeldByInflater - inflater.getRemaining(); - bufferBytesHeldByInflater -= toRelease; - source.skip(toRelease); - } - - @Override public Timeout timeout() { - return source.timeout(); - } - - @Override public void close() throws IOException { - if (closed) return; - inflater.end(); - closed = true; - source.close(); - } -} From 9953be594409605bef1c008fb0edd49a37c52415 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 13:50:14 -0700 Subject: [PATCH 057/399] purge okio! --- AndroidAsync/build.gradle | 9 +- .../async/http/spdy/AsyncSpdyConnection.java | 3 +- .../async/http/spdy/SpdyTransport.java | 1 - .../okhttp/internal/spdy/FrameReader.java | 4 +- .../spdy/okhttp/internal/spdy/Header.java | 2 +- .../okhttp/internal/spdy/HeaderReader.java | 2 +- .../okhttp/internal/spdy/HpackDraft08.java | 2 +- .../okhttp/internal/spdy/PushObserver.java | 96 -- .../http/spdy/okhttp/internal/spdy/Spdy3.java | 2 +- .../async/http/spdy/okio/AsyncTimeout.java | 318 ------ .../async/http/spdy/okio/Base64.java | 147 --- .../async/http/spdy/okio/Buffer.java | 911 ------------------ .../async/http/spdy/okio/BufferedSink.java | 82 -- .../async/http/spdy/okio/BufferedSource.java | 171 ---- .../async/http/spdy/okio/ByteString.java | 283 ------ .../async/http/spdy/okio/Okio.java | 194 ---- .../http/spdy/okio/RealBufferedSink.java | 207 ---- .../http/spdy/okio/RealBufferedSource.java | 301 ------ .../async/http/spdy/okio/Segment.java | 135 --- .../async/http/spdy/okio/SegmentPool.java | 64 -- .../async/http/spdy/okio/Sink.java | 66 -- .../async/http/spdy/okio/Source.java | 78 -- .../async/http/spdy/okio/Timeout.java | 153 --- .../async/http/spdy/okio/Util.java | 72 -- 24 files changed, 13 insertions(+), 3290 deletions(-) delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/PushObserver.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/AsyncTimeout.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Base64.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSink.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSource.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ByteString.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Okio.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSink.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSource.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Sink.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Source.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Timeout.java delete mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Util.java diff --git a/AndroidAsync/build.gradle b/AndroidAsync/build.gradle index e193153d7..77970b0f0 100644 --- a/AndroidAsync/build.gradle +++ b/AndroidAsync/build.gradle @@ -25,8 +25,13 @@ android { androidTest.assets.srcDirs=['test/assets/'] } - lintOptions { - abortOnError false +// lintOptions { +// abortOnError false +// } + + defaultConfig { + targetSdkVersion 21 + minSdkVersion 9 } compileSdkVersion 19 diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index 5c5f95c35..45117dc4a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -10,6 +10,7 @@ import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.http.Protocol; +import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.ErrorCode; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameReader; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameWriter; @@ -20,8 +21,6 @@ import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3; import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Variant; -import com.koushikdutta.async.http.spdy.okio.BufferedSink; -import com.koushikdutta.async.http.spdy.okio.ByteString; import java.io.IOException; import java.util.Hashtable; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java index e915a0650..fb29b6a72 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java @@ -19,7 +19,6 @@ import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.spdy.okhttp.internal.Util; -import com.koushikdutta.async.http.spdy.okio.ByteString; import java.util.List; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java index 05d2107de..674d80c52 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java @@ -17,10 +17,8 @@ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; -import java.io.Closeable; -import java.io.IOException; import java.util.List; /** diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java index ba0244fcb..bf9aaeb53 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java @@ -1,7 +1,7 @@ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; -import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; /** HTTP header: the name is an ASCII string, but the value can be UTF-8. */ public final class Header { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java index b4aa5dcbd..0df4ec83a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java @@ -1,7 +1,7 @@ package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java index b58d02958..8ef4aa593 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java @@ -17,7 +17,7 @@ import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.http.spdy.okhttp.internal.BitArray; -import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/PushObserver.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/PushObserver.java deleted file mode 100644 index fcec1732b..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/PushObserver.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okhttp.internal.spdy; - -import com.koushikdutta.async.http.spdy.okio.BufferedSource; - -import java.io.IOException; -import java.util.List; - -/** - * {@link com.squareup.okhttp.Protocol#HTTP_2 HTTP/2} only. - * Processes server-initiated HTTP requests on the client. Implementations must - * quickly dispatch callbacks to avoid creating a bottleneck. - * - *

While {@link #onReset} may occur at any time, the following callbacks are - * expected in order, correlated by stream ID. - *

    - *
  • {@link #onRequest}
  • - *
  • {@link #onHeaders} (unless canceled)
  • - *
  • {@link #onData} (optional sequence of data frames)
  • - *
- * - *

As a stream ID is scoped to a single HTTP/2 connection, implementations - * which target multiple connections should expect repetition of stream IDs. - * - *

Return true to request cancellation of a pushed stream. Note that this - * does not guarantee future frames won't arrive on the stream ID. - */ -public interface PushObserver { - /** - * Describes the request that the server intends to push a response for. - * - * @param streamId server-initiated stream ID: an even number. - * @param requestHeaders minimally includes {@code :method}, {@code :scheme}, - * {@code :authority}, and (@code :path}. - */ - boolean onRequest(int streamId, List

requestHeaders); - - /** - * The response headers corresponding to a pushed request. When {@code last} - * is true, there are no data frames to follow. - * - * @param streamId server-initiated stream ID: an even number. - * @param responseHeaders minimally includes {@code :status}. - * @param last when true, there is no response data. - */ - boolean onHeaders(int streamId, List
responseHeaders, boolean last); - - /** - * A chunk of response data corresponding to a pushed request. This data - * must either be read or skipped. - * - * @param streamId server-initiated stream ID: an even number. - * @param source location of data corresponding with this stream ID. - * @param byteCount number of bytes to read or skip from the source. - * @param last when true, there are no data frames to follow. - */ - boolean onData(int streamId, BufferedSource source, int byteCount, boolean last) - throws IOException; - - /** Indicates the reason why this stream was canceled. */ - void onReset(int streamId, ErrorCode errorCode); - - PushObserver CANCEL = new PushObserver() { - - @Override public boolean onRequest(int streamId, List
requestHeaders) { - return true; - } - - @Override public boolean onHeaders(int streamId, List
responseHeaders, boolean last) { - return true; - } - - @Override public boolean onData(int streamId, BufferedSource source, int byteCount, - boolean last) throws IOException { - source.skip(byteCount); - return true; - } - - @Override public void onReset(int streamId, ErrorCode errorCode) { - } - }; -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java index 3db622365..52155ca04 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java @@ -21,7 +21,7 @@ import com.koushikdutta.async.DataEmitterReader; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.http.spdy.okio.ByteString; +import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; import com.koushikdutta.async.util.Charsets; import java.io.IOException; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/AsyncTimeout.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/AsyncTimeout.java deleted file mode 100644 index b0b46ff59..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/AsyncTimeout.java +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.IOException; -import java.io.InterruptedIOException; - -/** - * This timeout uses a background thread to take action exactly when the timeout - * occurs. Use this to implement timeouts where they aren't supported natively, - * such as to sockets that are blocked on writing. - * - *

Subclasses should override {@link #timedOut} to take action when a timeout - * occurs. This method will be invoked by the shared watchdog thread so it - * should not do any long-running operations. Otherwise we risk starving other - * timeouts from being triggered. - * - *

Use {@link #sink} and {@link #source} to apply this timeout to a stream. - * The returned value will apply the timeout to each operation on the wrapped - * stream. - * - *

Callers should call {@link #enter} before doing work that is subject to - * timeouts, and {@link #exit} afterwards. The return value of {@link #exit} - * indicates whether a timeout was triggered. Note that the call to {@link - * #timedOut} is asynchronous, and may be called after {@link #exit}. - */ -public class AsyncTimeout extends Timeout { - /** - * The watchdog thread processes a linked list of pending timeouts, sorted in - * the order to be triggered. This class synchronizes on AsyncTimeout.class. - * This lock guards the queue. - * - *

Head's 'next' points to the first element of the linked list. The first - * element is the next node to time out, or null if the queue is empty. The - * head is null until the watchdog thread is started. - */ - private static AsyncTimeout head; - - /** True if this node is currently in the queue. */ - private boolean inQueue; - - /** The next node in the linked list. */ - private AsyncTimeout next; - - /** If scheduled, this is the time that the watchdog should time this out. */ - private long timeoutAt; - - public final void enter() { - if (inQueue) throw new IllegalStateException("Unbalanced enter/exit"); - long timeoutNanos = timeoutNanos(); - boolean hasDeadline = hasDeadline(); - if (timeoutNanos == 0 && !hasDeadline) { - return; // No timeout and no deadline? Don't bother with the queue. - } - inQueue = true; - scheduleTimeout(this, timeoutNanos, hasDeadline); - } - - private static synchronized void scheduleTimeout( - AsyncTimeout node, long timeoutNanos, boolean hasDeadline) { - // Start the watchdog thread and create the head node when the first timeout is scheduled. - if (head == null) { - head = new AsyncTimeout(); - new Watchdog().start(); - } - - long now = System.nanoTime(); - if (timeoutNanos != 0 && hasDeadline) { - // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap around, - // Math.min() is undefined for absolute values, but meaningful for relative ones. - node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now); - } else if (timeoutNanos != 0) { - node.timeoutAt = now + timeoutNanos; - } else if (hasDeadline) { - node.timeoutAt = node.deadlineNanoTime(); - } else { - throw new AssertionError(); - } - - // Insert the node in sorted order. - long remainingNanos = node.remainingNanos(now); - for (AsyncTimeout prev = head; true; prev = prev.next) { - if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) { - node.next = prev.next; - prev.next = node; - if (prev == head) { - AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front. - } - break; - } - } - } - - /** Returns true if the timeout occurred. */ - public final boolean exit() { - if (!inQueue) return false; - inQueue = false; - return cancelScheduledTimeout(this); - } - - /** Returns true if the timeout occurred. */ - private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) { - // Remove the node from the linked list. - for (AsyncTimeout prev = head; prev != null; prev = prev.next) { - if (prev.next == node) { - prev.next = node.next; - node.next = null; - return false; - } - } - - // The node wasn't found in the linked list: it must have timed out! - return true; - } - - /** - * Returns the amount of time left until the time out. This will be negative - * if the timeout has elapsed and the timeout should occur immediately. - */ - private long remainingNanos(long now) { - return timeoutAt - now; - } - - /** - * Invoked by the watchdog thread when the time between calls to {@link - * #enter()} and {@link #exit()} has exceeded the timeout. - */ - protected void timedOut() { - } - - /** - * Returns a new sink that delegates to {@code sink}, using this to implement - * timeouts. This works best if {@link #timedOut} is overridden to interrupt - * {@code sink}'s current operation. - */ - public final Sink sink(final Sink sink) { - return new Sink() { - @Override public void write(Buffer source, long byteCount) throws IOException { - boolean throwOnTimeout = false; - enter(); - try { - sink.write(source, byteCount); - throwOnTimeout = true; - } catch (IOException e) { - throw exit(e); - } finally { - exit(throwOnTimeout); - } - } - - @Override public void flush() throws IOException { - boolean throwOnTimeout = false; - enter(); - try { - sink.flush(); - throwOnTimeout = true; - } catch (IOException e) { - throw exit(e); - } finally { - exit(throwOnTimeout); - } - } - - @Override public void close() throws IOException { - boolean throwOnTimeout = false; - enter(); - try { - sink.close(); - throwOnTimeout = true; - } catch (IOException e) { - throw exit(e); - } finally { - exit(throwOnTimeout); - } - } - - @Override public Timeout timeout() { - return AsyncTimeout.this; - } - - @Override public String toString() { - return "AsyncTimeout.sink(" + sink + ")"; - } - }; - } - - /** - * Returns a new source that delegates to {@code source}, using this to - * implement timeouts. This works best if {@link #timedOut} is overridden to - * interrupt {@code sink}'s current operation. - */ - public final Source source(final Source source) { - return new Source() { - @Override public long read(Buffer sink, long byteCount) throws IOException { - boolean throwOnTimeout = false; - enter(); - try { - long result = source.read(sink, byteCount); - throwOnTimeout = true; - return result; - } catch (IOException e) { - throw exit(e); - } finally { - exit(throwOnTimeout); - } - } - - @Override public void close() throws IOException { - boolean throwOnTimeout = false; - try { - source.close(); - throwOnTimeout = true; - } catch (IOException e) { - throw exit(e); - } finally { - exit(throwOnTimeout); - } - } - - @Override public Timeout timeout() { - return AsyncTimeout.this; - } - - @Override public String toString() { - return "AsyncTimeout.source(" + source + ")"; - } - }; - } - - /** - * Throws an InterruptedIOException if {@code throwOnTimeout} is true and a - * timeout occurred. - */ - final void exit(boolean throwOnTimeout) throws IOException { - boolean timedOut = exit(); - if (timedOut && throwOnTimeout) throw new InterruptedIOException("timeout"); - } - - /** - * Returns either {@code cause} or an InterruptedIOException that's caused by - * {@code cause} if a timeout occurred. - */ - final IOException exit(IOException cause) throws IOException { - if (!exit()) return cause; - InterruptedIOException e = new InterruptedIOException("timeout"); - e.initCause(cause); - return e; - } - - private static final class Watchdog extends Thread { - public Watchdog() { - super("Okio Watchdog"); - setDaemon(true); - } - - public void run() { - while (true) { - try { - AsyncTimeout timedOut = awaitTimeout(); - - // Didn't find a node to interrupt. Try again. - if (timedOut == null) continue; - - // Close the timed out node. - timedOut.timedOut(); - } catch (InterruptedException ignored) { - } - } - } - } - - /** - * Removes and returns the node at the head of the list, waiting for it to - * time out if necessary. Returns null if the situation changes while waiting: - * either a newer node is inserted at the head, or the node being waited on - * has been removed. - */ - private static synchronized AsyncTimeout awaitTimeout() throws InterruptedException { - // Get the next eligible node. - AsyncTimeout node = head.next; - - // The queue is empty. Wait for something to be enqueued. - if (node == null) { - AsyncTimeout.class.wait(); - return null; - } - - long waitNanos = node.remainingNanos(System.nanoTime()); - - // The head of the queue hasn't timed out yet. Await that. - if (waitNanos > 0) { - // Waiting is made complicated by the fact that we work in nanoseconds, - // but the API wants (millis, nanos) in two arguments. - long waitMillis = waitNanos / 1000000L; - waitNanos -= (waitMillis * 1000000L); - AsyncTimeout.class.wait(waitMillis, (int) waitNanos); - return null; - } - - // The head of the queue has timed out. Remove it. - head.next = node.next; - node.next = null; - return node; - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Base64.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Base64.java deleted file mode 100644 index c0a6571f2..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Base64.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @author Alexander Y. Kleymenov - */ -package com.koushikdutta.async.http.spdy.okio; - -import java.io.UnsupportedEncodingException; - -final class Base64 { - private Base64() { - } - - public static byte[] decode(String in) { - // Ignore trailing '=' padding and whitespace from the input. - int limit = in.length(); - for (; limit > 0; limit--) { - char c = in.charAt(limit - 1); - if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') { - break; - } - } - - // If the input includes whitespace, this output array will be longer than necessary. - byte[] out = new byte[(int) (limit * 6L / 8L)]; - int outCount = 0; - int inCount = 0; - - int word = 0; - for (int pos = 0; pos < limit; pos++) { - char c = in.charAt(pos); - - int bits; - if (c >= 'A' && c <= 'Z') { - // char ASCII value - // A 65 0 - // Z 90 25 (ASCII - 65) - bits = c - 65; - } else if (c >= 'a' && c <= 'z') { - // char ASCII value - // a 97 26 - // z 122 51 (ASCII - 71) - bits = c - 71; - } else if (c >= '0' && c <= '9') { - // char ASCII value - // 0 48 52 - // 9 57 61 (ASCII + 4) - bits = c + 4; - } else if (c == '+') { - bits = 62; - } else if (c == '/') { - bits = 63; - } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') { - continue; - } else { - return null; - } - - // Append this char's 6 bits to the word. - word = (word << 6) | (byte) bits; - - // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes. - inCount++; - if (inCount % 4 == 0) { - out[outCount++] = (byte) (word >> 16); - out[outCount++] = (byte) (word >> 8); - out[outCount++] = (byte) word; - } - } - - int lastWordChars = inCount % 4; - if (lastWordChars == 1) { - // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail. - return null; - } else if (lastWordChars == 2) { - // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits. - word = word << 12; - out[outCount++] = (byte) (word >> 16); - } else if (lastWordChars == 3) { - // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits. - word = word << 6; - out[outCount++] = (byte) (word >> 16); - out[outCount++] = (byte) (word >> 8); - } - - // If we sized our out array perfectly, we're done. - if (outCount == out.length) return out; - - // Copy the decoded bytes to a new, right-sized array. - byte[] prefix = new byte[outCount]; - System.arraycopy(out, 0, prefix, 0, outCount); - return prefix; - } - - private static final byte[] MAP = new byte[] { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', - 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', - 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', - '5', '6', '7', '8', '9', '+', '/' - }; - - public static String encode(byte[] in) { - int length = (in.length + 2) * 4 / 3; - byte[] out = new byte[length]; - int index = 0, end = in.length - in.length % 3; - for (int i = 0; i < end; i += 3) { - out[index++] = MAP[(in[i] & 0xff) >> 2]; - out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; - out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; - out[index++] = MAP[(in[i + 2] & 0x3f)]; - } - switch (in.length % 3) { - case 1: - out[index++] = MAP[(in[end] & 0xff) >> 2]; - out[index++] = MAP[(in[end] & 0x03) << 4]; - out[index++] = '='; - out[index++] = '='; - break; - case 2: - out[index++] = MAP[(in[end] & 0xff) >> 2]; - out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; - out[index++] = MAP[((in[end + 1] & 0x0f) << 2)]; - out[index++] = '='; - break; - } - try { - return new String(out, 0, index, "US-ASCII"); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java deleted file mode 100644 index bb6852dc4..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Buffer.java +++ /dev/null @@ -1,911 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -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 static com.koushikdutta.async.http.spdy.okhttp.internal.Util.checkOffsetAndCount; -import static com.koushikdutta.async.http.spdy.okio.Util.reverseBytesLong; - -/** - * A collection of bytes in memory. - * - *

Moving data from one buffer to another is fast. Instead - * of copying bytes from one place in memory to another, this class just changes - * ownership of the underlying byte arrays. - * - *

This buffer grows with your data. Just like ArrayList, - * each buffer starts small. It consumes only the memory it needs to. - * - *

This buffer pools its byte arrays. When you allocate a - * byte array in Java, the runtime must zero-fill the requested array before - * returning it to you. Even if you're going to write over that space anyway. - * This class avoids zero-fill and GC churn by pooling byte arrays. - */ -public final class Buffer implements BufferedSource, BufferedSink, Cloneable { - public Segment head; - public long size; - - public Buffer() { - } - - /** Returns the number of bytes currently in this buffer. */ - public long size() { - return size; - } - - @Override public Buffer buffer() { - return this; - } - - @Override public OutputStream outputStream() { - return new OutputStream() { - @Override public void write(int b) { - writeByte((byte) b); - } - - @Override public void write(byte[] data, int offset, int byteCount) { - Buffer.this.write(data, offset, byteCount); - } - - @Override public void flush() { - } - - @Override public void close() { - } - - @Override public String toString() { - return this + ".outputStream()"; - } - }; - } - - @Override public Buffer emitCompleteSegments() { - return this; // Nowhere to emit to! - } - - @Override public boolean exhausted() { - return size == 0; - } - - @Override public void require(long byteCount) throws EOFException { - if (this.size < byteCount) throw new EOFException(); - } - - @Override public InputStream inputStream() { - return new InputStream() { - @Override public int read() { - if (size > 0) return readByte() & 0xff; - return -1; - } - - @Override public int read(byte[] sink, int offset, int byteCount) { - return Buffer.this.read(sink, offset, byteCount); - } - - @Override public int available() { - return (int) Math.min(size, Integer.MAX_VALUE); - } - - @Override public void close() { - } - - @Override public String toString() { - return Buffer.this + ".inputStream()"; - } - }; - } - - /** Copy the contents of this to {@code out}. */ - public Buffer copyTo(OutputStream out) throws IOException { - return copyTo(out, 0, size); - } - - /** - * Copy {@code byteCount} bytes from this, starting at {@code offset}, to - * {@code out}. - */ - public Buffer copyTo(OutputStream out, long offset, long byteCount) throws IOException { - if (out == null) throw new IllegalArgumentException("out == null"); - checkOffsetAndCount(size, offset, byteCount); - if (byteCount == 0) return this; - - // Skip segments that we aren't copying from. - Segment s = head; - for (; offset >= (s.limit - s.pos); s = s.next) { - offset -= (s.limit - s.pos); - } - - // Copy from one segment at a time. - for (; byteCount > 0; s = s.next) { - int pos = (int) (s.pos + offset); - int toWrite = (int) Math.min(s.limit - pos, byteCount); - out.write(s.data, pos, toWrite); - byteCount -= toWrite; - offset = 0; - } - - return this; - } - - /** Write the contents of this to {@code out}. */ - public Buffer writeTo(OutputStream out) throws IOException { - return writeTo(out, size); - } - - /** Write {@code byteCount} bytes from this to {@code out}. */ - public Buffer writeTo(OutputStream out, long byteCount) throws IOException { - if (out == null) throw new IllegalArgumentException("out == null"); - checkOffsetAndCount(size, 0, byteCount); - - Segment s = head; - while (byteCount > 0) { - int toCopy = (int) Math.min(byteCount, s.limit - s.pos); - out.write(s.data, s.pos, toCopy); - - s.pos += toCopy; - size -= toCopy; - byteCount -= toCopy; - - if (s.pos == s.limit) { - Segment toRecycle = s; - head = s = toRecycle.pop(); - SegmentPool.getInstance().recycle(toRecycle); - } - } - - return this; - } - - /** Read and exhaust bytes from {@code in} to this. */ - public Buffer readFrom(InputStream in) throws IOException { - readFrom(in, Long.MAX_VALUE, true); - return this; - } - - /** Read {@code byteCount} bytes from {@code in} to this. */ - public Buffer readFrom(InputStream in, long byteCount) throws IOException { - if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); - readFrom(in, byteCount, false); - return this; - } - - private void readFrom(InputStream in, long byteCount, boolean forever) throws IOException { - if (in == null) throw new IllegalArgumentException("in == null"); - while (byteCount > 0 || forever) { - Segment tail = writableSegment(1); - int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit); - int bytesRead = in.read(tail.data, tail.limit, maxToCopy); - if (bytesRead == -1) { - if (forever) return; - throw new EOFException(); - } - tail.limit += bytesRead; - size += bytesRead; - byteCount -= bytesRead; - } - } - - /** - * Returns the number of bytes in segments that are not writable. This is the - * number of bytes that can be flushed immediately to an underlying sink - * without harming throughput. - */ - public long completeSegmentByteCount() { - long result = size; - if (result == 0) return 0; - - // Omit the tail if it's still writable. - Segment tail = head.prev; - if (tail.limit < Segment.SIZE) { - result -= tail.limit - tail.pos; - } - - return result; - } - - @Override public byte readByte() { - if (size == 0) throw new IllegalStateException("size == 0"); - - Segment segment = head; - int pos = segment.pos; - int limit = segment.limit; - - byte[] data = segment.data; - byte b = data[pos++]; - size -= 1; - - if (pos == limit) { - head = segment.pop(); - SegmentPool.getInstance().recycle(segment); - } else { - segment.pos = pos; - } - - return b; - } - - /** Returns the byte at {@code pos}. */ - public byte getByte(long pos) { - checkOffsetAndCount(size, pos, 1); - for (Segment s = head; true; s = s.next) { - int segmentByteCount = s.limit - s.pos; - if (pos < segmentByteCount) return s.data[s.pos + (int) pos]; - pos -= segmentByteCount; - } - } - - @Override public short readShort() { - if (size < 2) throw new IllegalStateException("size < 2: " + size); - - Segment segment = head; - int pos = segment.pos; - int limit = segment.limit; - - // If the short is split across multiple segments, delegate to readByte(). - if (limit - pos < 2) { - int s = (readByte() & 0xff) << 8 - | (readByte() & 0xff); - return (short) s; - } - - byte[] data = segment.data; - int s = (data[pos++] & 0xff) << 8 - | (data[pos++] & 0xff); - size -= 2; - - if (pos == limit) { - head = segment.pop(); - SegmentPool.getInstance().recycle(segment); - } else { - segment.pos = pos; - } - - return (short) s; - } - - @Override public int readInt() { - if (size < 4) throw new IllegalStateException("size < 4: " + size); - - Segment segment = head; - int pos = segment.pos; - int limit = segment.limit; - - // If the int is split across multiple segments, delegate to readByte(). - if (limit - pos < 4) { - return (readByte() & 0xff) << 24 - | (readByte() & 0xff) << 16 - | (readByte() & 0xff) << 8 - | (readByte() & 0xff); - } - - byte[] data = segment.data; - int i = (data[pos++] & 0xff) << 24 - | (data[pos++] & 0xff) << 16 - | (data[pos++] & 0xff) << 8 - | (data[pos++] & 0xff); - size -= 4; - - if (pos == limit) { - head = segment.pop(); - SegmentPool.getInstance().recycle(segment); - } else { - segment.pos = pos; - } - - return i; - } - - @Override public long readLong() { - if (size < 8) throw new IllegalStateException("size < 8: " + size); - - Segment segment = head; - int pos = segment.pos; - int limit = segment.limit; - - // If the long is split across multiple segments, delegate to readInt(). - if (limit - pos < 8) { - return (readInt() & 0xffffffffL) << 32 - | (readInt() & 0xffffffffL); - } - - byte[] data = segment.data; - long v = (data[pos++] & 0xffL) << 56 - | (data[pos++] & 0xffL) << 48 - | (data[pos++] & 0xffL) << 40 - | (data[pos++] & 0xffL) << 32 - | (data[pos++] & 0xffL) << 24 - | (data[pos++] & 0xffL) << 16 - | (data[pos++] & 0xffL) << 8 - | (data[pos++] & 0xffL); - size -= 8; - - if (pos == limit) { - head = segment.pop(); - SegmentPool.getInstance().recycle(segment); - } else { - segment.pos = pos; - } - - return v; - } - - @Override public short readShortLe() { - return Util.reverseBytesShort(readShort()); - } - - @Override public int readIntLe() { - return Util.reverseBytesInt(readInt()); - } - - @Override public long readLongLe() { - return Util.reverseBytesLong(readLong()); - } - - @Override public ByteString readByteString() { - return new ByteString(readByteArray()); - } - - @Override public ByteString readByteString(long byteCount) throws EOFException { - return new ByteString(readByteArray(byteCount)); - } - - @Override public void readFully(Buffer sink, long byteCount) throws EOFException { - if (size < byteCount) { - sink.write(this, size); // Exhaust ourselves. - throw new EOFException(); - } - sink.write(this, byteCount); - } - - @Override public long readAll(Sink sink) throws IOException { - long byteCount = size; - if (byteCount > 0) { - sink.write(this, byteCount); - } - return byteCount; - } - - @Override public String readUtf8() { - try { - return readString(size, Util.UTF_8); - } catch (EOFException e) { - throw new AssertionError(e); - } - } - - @Override public String readUtf8(long byteCount) throws EOFException { - return readString(byteCount, Util.UTF_8); - } - - @Override public String readString(Charset charset) { - try { - return readString(size, charset); - } catch (EOFException e) { - throw new AssertionError(e); - } - } - - @Override public String readString(long byteCount, Charset charset) throws EOFException { - checkOffsetAndCount(size, 0, byteCount); - if (charset == null) throw new IllegalArgumentException("charset == null"); - if (byteCount > Integer.MAX_VALUE) { - throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount); - } - if (byteCount == 0) return ""; - - Segment head = this.head; - if (head.pos + byteCount > head.limit) { - // If the string spans multiple segments, delegate to readBytes(). - return new String(readByteArray(byteCount), charset); - } - - String result = new String(head.data, head.pos, (int) byteCount, charset); - head.pos += byteCount; - size -= byteCount; - - if (head.pos == head.limit) { - this.head = head.pop(); - SegmentPool.getInstance().recycle(head); - } - - return result; - } - - @Override public String readUtf8Line() throws EOFException { - long newline = indexOf((byte) '\n'); - - if (newline == -1) { - return size != 0 ? readUtf8(size) : null; - } - - return readUtf8Line(newline); - } - - @Override public String readUtf8LineStrict() throws EOFException { - long newline = indexOf((byte) '\n'); - if (newline == -1) throw new EOFException(); - return readUtf8Line(newline); - } - - String readUtf8Line(long newline) throws EOFException { - if (newline > 0 && getByte(newline - 1) == '\r') { - // Read everything until '\r\n', then skip the '\r\n'. - String result = readUtf8((newline - 1)); - skip(2); - return result; - - } else { - // Read everything until '\n', then skip the '\n'. - String result = readUtf8(newline); - skip(1); - return result; - } - } - - @Override public byte[] readByteArray() { - try { - return readByteArray(size); - } catch (EOFException e) { - throw new AssertionError(e); - } - } - - @Override public byte[] readByteArray(long byteCount) throws EOFException { - checkOffsetAndCount(this.size, 0, byteCount); - if (byteCount > Integer.MAX_VALUE) { - throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount); - } - - byte[] result = new byte[(int) byteCount]; - readFully(result); - return result; - } - - @Override public int read(byte[] sink) { - return read(sink, 0, sink.length); - } - - @Override public void readFully(byte[] sink) throws EOFException { - int offset = 0; - while (offset < sink.length) { - int read = read(sink, offset, sink.length - offset); - if (read == -1) throw new EOFException(); - offset += read; - } - } - - @Override public int read(byte[] sink, int offset, int byteCount) { - checkOffsetAndCount(sink.length, offset, byteCount); - - Segment s = this.head; - if (s == null) return -1; - int toCopy = Math.min(byteCount, s.limit - s.pos); - System.arraycopy(s.data, s.pos, sink, offset, toCopy); - - s.pos += toCopy; - this.size -= toCopy; - - if (s.pos == s.limit) { - this.head = s.pop(); - SegmentPool.getInstance().recycle(s); - } - - return toCopy; - } - - /** - * Discards all bytes in this buffer. Calling this method when you're done - * with a buffer will return its segments to the pool. - */ - public void clear() { - try { - skip(size); - } catch (EOFException e) { - throw new AssertionError(e); - } - } - - /** Discards {@code byteCount} bytes from the head of this buffer. */ - @Override public void skip(long byteCount) throws EOFException { - while (byteCount > 0) { - if (head == null) throw new EOFException(); - - int toSkip = (int) Math.min(byteCount, head.limit - head.pos); - size -= toSkip; - byteCount -= toSkip; - head.pos += toSkip; - - if (head.pos == head.limit) { - Segment toRecycle = head; - head = toRecycle.pop(); - SegmentPool.getInstance().recycle(toRecycle); - } - } - } - - @Override public Buffer write(ByteString byteString) { - if (byteString == null) throw new IllegalArgumentException("byteString == null"); - return write(byteString.data, 0, byteString.data.length); - } - - @Override public Buffer writeUtf8(String string) { - if (string == null) throw new IllegalArgumentException("string == null"); - // TODO: inline UTF-8 encoding to save allocating a byte[]? - return writeString(string, Util.UTF_8); - } - - @Override public Buffer writeString(String string, Charset charset) { - if (string == null) throw new IllegalArgumentException("string == null"); - if (charset == null) throw new IllegalArgumentException("charset == null"); - byte[] data = string.getBytes(charset); - return write(data, 0, data.length); - } - - @Override public Buffer write(byte[] source) { - if (source == null) throw new IllegalArgumentException("source == null"); - return write(source, 0, source.length); - } - - @Override public Buffer write(byte[] source, int offset, int byteCount) { - if (source == null) throw new IllegalArgumentException("source == null"); - checkOffsetAndCount(source.length, offset, byteCount); - - int limit = offset + byteCount; - while (offset < limit) { - Segment tail = writableSegment(1); - - int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit); - System.arraycopy(source, offset, tail.data, tail.limit, toCopy); - - offset += toCopy; - tail.limit += toCopy; - } - - this.size += byteCount; - return this; - } - - @Override public long writeAll(Source source) throws IOException { - if (source == null) throw new IllegalArgumentException("source == null"); - long totalBytesRead = 0; - for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) { - totalBytesRead += readCount; - } - return totalBytesRead; - } - - @Override public Buffer writeByte(int b) { - Segment tail = writableSegment(1); - tail.data[tail.limit++] = (byte) b; - size += 1; - return this; - } - - @Override public Buffer writeShort(int s) { - Segment tail = writableSegment(2); - byte[] data = tail.data; - int limit = tail.limit; - data[limit++] = (byte) ((s >>> 8) & 0xff); - data[limit++] = (byte) (s & 0xff); - tail.limit = limit; - size += 2; - return this; - } - - @Override public Buffer writeShortLe(int s) { - return writeShort(Util.reverseBytesShort((short) s)); - } - - @Override public Buffer writeInt(int i) { - Segment tail = writableSegment(4); - byte[] data = tail.data; - int limit = tail.limit; - data[limit++] = (byte) ((i >>> 24) & 0xff); - data[limit++] = (byte) ((i >>> 16) & 0xff); - data[limit++] = (byte) ((i >>> 8) & 0xff); - data[limit++] = (byte) (i & 0xff); - tail.limit = limit; - size += 4; - return this; - } - - @Override public Buffer writeIntLe(int i) { - return writeInt(Util.reverseBytesInt(i)); - } - - @Override public Buffer writeLong(long v) { - Segment tail = writableSegment(8); - byte[] data = tail.data; - int limit = tail.limit; - data[limit++] = (byte) ((v >>> 56L) & 0xff); - data[limit++] = (byte) ((v >>> 48L) & 0xff); - data[limit++] = (byte) ((v >>> 40L) & 0xff); - data[limit++] = (byte) ((v >>> 32L) & 0xff); - data[limit++] = (byte) ((v >>> 24L) & 0xff); - data[limit++] = (byte) ((v >>> 16L) & 0xff); - data[limit++] = (byte) ((v >>> 8L) & 0xff); - data[limit++] = (byte) (v & 0xff); - tail.limit = limit; - size += 8; - return this; - } - - @Override public Buffer writeLongLe(long v) { - return writeLong(reverseBytesLong(v)); - } - - /** - * Returns a tail segment that we can write at least {@code minimumCapacity} - * bytes to, creating it if necessary. - */ - Segment writableSegment(int minimumCapacity) { - if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException(); - - if (head == null) { - head = SegmentPool.getInstance().take(); // Acquire a first segment. - return head.next = head.prev = head; - } - - Segment tail = head.prev; - if (tail.limit + minimumCapacity > Segment.SIZE) { - tail = tail.push(SegmentPool.getInstance().take()); // Append a new empty segment to fill up. - } - return tail; - } - - @Override public void write(Buffer source, long byteCount) { - // Move bytes from the head of the source buffer to the tail of this buffer - // while balancing two conflicting goals: don't waste CPU and don't waste - // memory. - // - // - // Don't waste CPU (ie. don't copy data around). - // - // Copying large amounts of data is expensive. Instead, we prefer to - // reassign entire segments from one buffer to the other. - // - // - // Don't waste memory. - // - // As an invariant, adjacent pairs of segments in a buffer should be at - // least 50% full, except for the head segment and the tail segment. - // - // The head segment cannot maintain the invariant because the application is - // consuming bytes from this segment, decreasing its level. - // - // The tail segment cannot maintain the invariant because the application is - // producing bytes, which may require new nearly-empty tail segments to be - // appended. - // - // - // Moving segments between buffers - // - // When writing one buffer to another, we prefer to reassign entire segments - // over copying bytes into their most compact form. Suppose we have a buffer - // with these segment levels [91%, 61%]. If we append a buffer with a - // single [72%] segment, that yields [91%, 61%, 72%]. No bytes are copied. - // - // Or suppose we have a buffer with these segment levels: [100%, 2%], and we - // want to append it to a buffer with these segment levels [99%, 3%]. This - // operation will yield the following segments: [100%, 2%, 99%, 3%]. That - // is, we do not spend time copying bytes around to achieve more efficient - // memory use like [100%, 100%, 4%]. - // - // When combining buffers, we will compact adjacent buffers when their - // combined level doesn't exceed 100%. For example, when we start with - // [100%, 40%] and append [30%, 80%], the result is [100%, 70%, 80%]. - // - // - // Splitting segments - // - // Occasionally we write only part of a source buffer to a sink buffer. For - // example, given a sink [51%, 91%], we may want to write the first 30% of - // a source [92%, 82%] to it. To simplify, we first transform the source to - // an equivalent buffer [30%, 62%, 82%] and then move the head segment, - // yielding sink [51%, 91%, 30%] and source [62%, 82%]. - - if (source == null) throw new IllegalArgumentException("source == null"); - if (source == this) throw new IllegalArgumentException("source == this"); - checkOffsetAndCount(source.size, 0, byteCount); - - while (byteCount > 0) { - // Is a prefix of the source's head segment all that we need to move? - if (byteCount < (source.head.limit - source.head.pos)) { - Segment tail = head != null ? head.prev : null; - if (tail == null || byteCount + (tail.limit - tail.pos) > Segment.SIZE) { - // We're going to need another segment. Split the source's head - // segment in two, then move the first of those two to this buffer. - source.head = source.head.split((int) byteCount); - } else { - // Our existing segments are sufficient. Move bytes from source's head to our tail. - source.head.writeTo(tail, (int) byteCount); - source.size -= byteCount; - this.size += byteCount; - return; - } - } - - // Remove the source's head segment and append it to our tail. - Segment segmentToMove = source.head; - long movedByteCount = segmentToMove.limit - segmentToMove.pos; - source.head = segmentToMove.pop(); - if (head == null) { - head = segmentToMove; - head.next = head.prev = head; - } else { - Segment tail = head.prev; - tail = tail.push(segmentToMove); - tail.compact(); - } - source.size -= movedByteCount; - this.size += movedByteCount; - byteCount -= movedByteCount; - } - } - - @Override public long read(Buffer sink, long byteCount) { - if (sink == null) throw new IllegalArgumentException("sink == null"); - if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); - if (this.size == 0) return -1L; - if (byteCount > this.size) byteCount = this.size; - sink.write(this, byteCount); - return byteCount; - } - - @Override public long indexOf(byte b) { - return indexOf(b, 0); - } - - /** - * Returns the index of {@code b} in this at or beyond {@code fromIndex}, or - * -1 if this buffer does not contain {@code b} in that range. - */ - public long indexOf(byte b, long fromIndex) { - if (fromIndex < 0) throw new IllegalArgumentException("fromIndex < 0"); - - Segment s = head; - if (s == null) return -1L; - long offset = 0L; - do { - int segmentByteCount = s.limit - s.pos; - if (fromIndex >= segmentByteCount) { - fromIndex -= segmentByteCount; - } else { - byte[] data = s.data; - for (long pos = s.pos + fromIndex, limit = s.limit; pos < limit; pos++) { - if (data[(int) pos] == b) return offset + pos - s.pos; - } - fromIndex = 0; - } - offset += segmentByteCount; - s = s.next; - } while (s != head); - return -1L; - } - - @Override public void flush() { - } - - @Override public void close() { - } - - @Override public Timeout timeout() { - return Timeout.NONE; - } - - /** For testing. This returns the sizes of the segments in this buffer. */ - List segmentSizes() { - if (head == null) return Collections.emptyList(); - List result = new ArrayList(); - result.add(head.limit - head.pos); - for (Segment s = head.next; s != head; s = s.next) { - result.add(s.limit - s.pos); - } - return result; - } - - @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Buffer)) return false; - Buffer that = (Buffer) o; - if (size != that.size) return false; - if (size == 0) return true; // Both buffers are empty. - - Segment sa = this.head; - Segment sb = that.head; - int posA = sa.pos; - int posB = sb.pos; - - for (long pos = 0, count; pos < size; pos += count) { - count = Math.min(sa.limit - posA, sb.limit - posB); - - for (int i = 0; i < count; i++) { - if (sa.data[posA++] != sb.data[posB++]) return false; - } - - if (posA == sa.limit) { - sa = sa.next; - posA = sa.pos; - } - - if (posB == sb.limit) { - sb = sb.next; - posB = sb.pos; - } - } - - return true; - } - - @Override public int hashCode() { - Segment s = head; - if (s == null) return 0; - int result = 1; - do { - for (int pos = s.pos, limit = s.limit; pos < limit; pos++) { - result = 31 * result + s.data[pos]; - } - s = s.next; - } while (s != head); - return result; - } - - @Override public String toString() { - if (size == 0) { - return "Buffer[size=0]"; - } - - if (size <= 16) { - ByteString data = clone().readByteString(); - return String.format("Buffer[size=%s data=%s]", size, data.hex()); - } - - try { - MessageDigest md5 = MessageDigest.getInstance("MD5"); - md5.update(head.data, head.pos, head.limit - head.pos); - for (Segment s = head.next; s != head; s = s.next) { - md5.update(s.data, s.pos, s.limit - s.pos); - } - return String.format("Buffer[size=%s md5=%s]", - size, ByteString.of(md5.digest()).hex()); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(); - } - } - - /** Returns a deep copy of this buffer. */ - @Override public Buffer clone() { - Buffer result = new Buffer(); - if (size == 0) return result; - - result.write(head.data, head.pos, head.limit - head.pos); - for (Segment s = head.next; s != head; s = s.next) { - result.write(s.data, s.pos, s.limit - s.pos); - } - - return result; - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSink.java deleted file mode 100644 index 777840389..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSink.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.Charset; - -/** - * A sink that keeps a buffer internally so that callers can do small writes - * without a performance penalty. - */ -public interface BufferedSink extends Sink { - /** Returns this sink's internal buffer. */ - Buffer buffer(); - - BufferedSink write(ByteString byteString) throws IOException; - - /** - * Like {@link java.io.OutputStream#write(byte[])}, this writes a complete byte array to - * this sink. - */ - BufferedSink write(byte[] source) throws IOException; - - /** - * Like {@link java.io.OutputStream#write(byte[], int, int)}, this writes {@code byteCount} - * bytes of {@code source}, starting at {@code offset}. - */ - BufferedSink write(byte[] source, int offset, int byteCount) throws IOException; - - /** - * Removes all bytes from {@code source} and appends them to this. Returns the - * number of bytes read which will be 0 if {@code source} is exhausted. - */ - long writeAll(Source source) throws IOException; - - /** Encodes {@code string} in UTF-8 and writes it to this sink. */ - BufferedSink writeUtf8(String string) throws IOException; - - /** Encodes {@code string} in {@code charset} and writes it to this sink. */ - BufferedSink writeString(String string, Charset charset) throws IOException; - - /** Writes a byte to this sink. */ - BufferedSink writeByte(int b) throws IOException; - - /** Writes a big-endian short to this sink using two bytes. */ - BufferedSink writeShort(int s) throws IOException; - - /** Writes a little-endian short to this sink using two bytes. */ - BufferedSink writeShortLe(int s) throws IOException; - - /** Writes a big-endian int to this sink using four bytes. */ - BufferedSink writeInt(int i) throws IOException; - - /** Writes a little-endian int to this sink using four bytes. */ - BufferedSink writeIntLe(int i) throws IOException; - - /** Writes a big-endian long to this sink using eight bytes. */ - BufferedSink writeLong(long v) throws IOException; - - /** Writes a little-endian long to this sink using eight bytes. */ - BufferedSink writeLongLe(long v) throws IOException; - - /** Writes complete segments to this sink. Like {@link #flush}, but weaker. */ - BufferedSink emitCompleteSegments() throws IOException; - - /** Returns an output stream that writes to this sink. */ - OutputStream outputStream(); -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSource.java deleted file mode 100644 index af0129488..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/BufferedSource.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; - -/** - * A source that keeps a buffer internally so that callers can do small reads - * without a performance penalty. It also allows clients to read ahead, - * buffering as much as necessary before consuming input. - */ -public interface BufferedSource extends Source { - /** Returns this source's internal buffer. */ - Buffer buffer(); - - /** - * Returns true if there are no more bytes in this source. This will block - * until there are bytes to read or the source is definitely exhausted. - */ - boolean exhausted() throws IOException; - - /** - * Returns when the buffer contains at least {@code byteCount} bytes. Throws - * an {@link java.io.EOFException} if the source is exhausted before the - * required bytes can be read. - */ - void require(long byteCount) throws IOException; - - /** Removes a byte from this source and returns it. */ - byte readByte() throws IOException; - - /** Removes two bytes from this source and returns a big-endian short. */ - short readShort() throws IOException; - - /** Removes two bytes from this source and returns a little-endian short. */ - short readShortLe() throws IOException; - - /** Removes four bytes from this source and returns a big-endian int. */ - int readInt() throws IOException; - - /** Removes four bytes from this source and returns a little-endian int. */ - int readIntLe() throws IOException; - - /** Removes eight bytes from this source and returns a big-endian long. */ - long readLong() throws IOException; - - /** Removes eight bytes from this source and returns a little-endian long. */ - long readLongLe() throws IOException; - - /** - * Reads and discards {@code byteCount} bytes from this source. Throws an - * {@link java.io.EOFException} if the source is exhausted before the - * requested bytes can be skipped. - */ - void skip(long byteCount) throws IOException; - - /** Removes all bytes bytes from this and returns them as a byte string. */ - ByteString readByteString() throws IOException; - - /** Removes {@code byteCount} bytes from this and returns them as a byte string. */ - ByteString readByteString(long byteCount) throws IOException; - - /** Removes all bytes from this and returns them as a byte array. */ - byte[] readByteArray() throws IOException; - - /** Removes {@code byteCount} bytes from this and returns them as a byte array. */ - byte[] readByteArray(long byteCount) throws IOException; - - /** - * Removes up to {@code sink.length} bytes from this and copies them into {@code sink}. - * Returns the number of bytes read, or -1 if this source is exhausted. - */ - int read(byte[] sink) throws IOException; - - /** - * Removes exactly {@code sink.length} bytes from this and copies them into {@code sink}. - * Throws an {@link java.io.EOFException} if the requested number of bytes cannot be read. - */ - void readFully(byte[] sink) throws IOException; - - /** - * Removes up to {@code byteCount} bytes from this and copies them into {@code sink} at - * {@code offset}. Returns the number of bytes read, or -1 if this source is exhausted. - */ - int read(byte[] sink, int offset, int byteCount) throws IOException; - - /** - * Removes exactly {@code byteCount} bytes from this and appends them to - * {@code sink}. Throws an {@link java.io.EOFException} if the requested - * number of bytes cannot be read. - */ - void readFully(Buffer sink, long byteCount) throws IOException; - - /** - * Removes all bytes from this and appends them to {@code sink}. Returns the - * total number of bytes written to {@code sink} which will be 0 if this is - * exhausted. - */ - long readAll(Sink sink) throws IOException; - - /** Removes all bytes from this, decodes them as UTF-8, and returns the string. */ - String readUtf8() throws IOException; - - /** - * Removes {@code byteCount} bytes from this, decodes them as UTF-8, and - * returns the string. - */ - String readUtf8(long byteCount) throws IOException; - - /** - * Removes and returns characters up to but not including the next line break. - * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are - * not included in the result. - * - *

On the end of the stream this method returns null, just - * like {@link java.io.BufferedReader}. If the source doesn't end with a line - * break then an implicit line break is assumed. Null is returned once the - * source is exhausted. Use this for human-generated data, where a trailing - * line break is optional. - */ - String readUtf8Line() throws IOException; - - /** - * Removes and returns characters up to but not including the next line break. - * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are - * not included in the result. - * - *

On the end of the stream this method throws. Every call - * must consume either '\r\n' or '\n'. If these characters are absent in the - * stream, an {@link java.io.EOFException} is thrown. Use this for - * machine-generated data where a missing line break implies truncated input. - */ - String readUtf8LineStrict() throws IOException; - - /** - * Removes all bytes from this, decodes them as {@code charset}, and returns - * the string. - */ - String readString(Charset charset) throws IOException; - - /** - * Removes {@code byteCount} bytes from this, decodes them as {@code charset}, - * and returns the string. - */ - String readString(long byteCount, Charset charset) throws IOException; - - /** - * Returns the index of {@code b} in the buffer, refilling it if necessary - * until it is found. This reads an unbounded number of bytes into the buffer. - * Returns -1 if the stream is exhausted before the requested byte is found. - */ - long indexOf(byte b) throws IOException; - - /** Returns an input stream that reads from this source. */ - InputStream inputStream(); -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ByteString.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ByteString.java deleted file mode 100644 index c029e1ee8..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/ByteString.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.OutputStream; -import java.io.Serializable; -import java.lang.reflect.Field; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import static com.koushikdutta.async.http.spdy.okio.Util.checkOffsetAndCount; - -/** - * An immutable sequence of bytes. - * - *

Full disclosure: this class provides untrusted input and - * output streams with raw access to the underlying byte array. A hostile - * stream implementation could keep a reference to the mutable byte string, - * violating the immutable guarantee of this class. For this reason a byte - * string's immutability guarantee cannot be relied upon for security in applets - * and other environments that run both trusted and untrusted code in the same - * process. - */ -public final class ByteString implements Serializable { - private static final char[] HEX_DIGITS = - { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; - private static final long serialVersionUID = 1L; - - /** A singleton empty {@code ByteString}. */ - public static final ByteString EMPTY = ByteString.of(); - - final byte[] data; - private transient int hashCode; // Lazily computed; 0 if unknown. - private transient String utf8; // Lazily computed. - - ByteString(byte[] data) { - this.data = data; // Trusted internal constructor doesn't clone data. - } - - /** - * Returns a new byte string containing a clone of the bytes of {@code data}. - */ - public static ByteString of(byte... data) { - if (data == null) throw new IllegalArgumentException("data == null"); - return new ByteString(data.clone()); - } - - /** - * Returns a new byte string containing a copy of {@code byteCount} bytes of {@code data} starting - * at {@code offset}. - */ - public static ByteString of(byte[] data, int offset, int byteCount) { - if (data == null) throw new IllegalArgumentException("data == null"); - checkOffsetAndCount(data.length, offset, byteCount); - - byte[] copy = new byte[byteCount]; - System.arraycopy(data, offset, copy, 0, byteCount); - return new ByteString(copy); - } - - /** Returns a new byte string containing the {@code UTF-8} bytes of {@code s}. */ - public static ByteString encodeUtf8(String s) { - if (s == null) throw new IllegalArgumentException("s == null"); - ByteString byteString = new ByteString(s.getBytes(Util.UTF_8)); - byteString.utf8 = s; - return byteString; - } - - /** Constructs a new {@code String} by decoding the bytes as {@code UTF-8}. */ - public String utf8() { - String result = utf8; - // We don't care if we double-allocate in racy code. - return result != null ? result : (utf8 = new String(data, Util.UTF_8)); - } - - /** - * Returns this byte string encoded as Base64. In violation of the - * RFC, the returned string does not wrap lines at 76 columns. - */ - public String base64() { - return Base64.encode(data); - } - - /** - * Decodes the Base64-encoded bytes and returns their value as a byte string. - * Returns null if {@code base64} is not a Base64-encoded sequence of bytes. - */ - public static ByteString decodeBase64(String base64) { - if (base64 == null) throw new IllegalArgumentException("base64 == null"); - byte[] decoded = Base64.decode(base64); - return decoded != null ? new ByteString(decoded) : null; - } - - /** Returns this byte string encoded in hexadecimal. */ - public String hex() { - char[] result = new char[data.length * 2]; - int c = 0; - for (byte b : data) { - result[c++] = HEX_DIGITS[(b >> 4) & 0xf]; - result[c++] = HEX_DIGITS[b & 0xf]; - } - return new String(result); - } - - /** Decodes the hex-encoded bytes and returns their value a byte string. */ - public static ByteString decodeHex(String hex) { - if (hex == null) throw new IllegalArgumentException("hex == null"); - if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex); - - byte[] result = new byte[hex.length() / 2]; - for (int i = 0; i < result.length; i++) { - int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4; - int d2 = decodeHexDigit(hex.charAt(i * 2 + 1)); - result[i] = (byte) (d1 + d2); - } - return of(result); - } - - private static int decodeHexDigit(char c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - throw new IllegalArgumentException("Unexpected hex digit: " + c); - } - - /** - * Reads {@code count} bytes from {@code in} and returns the result. - * - * @throws java.io.EOFException if {@code in} has fewer than {@code count} - * bytes to read. - */ - public static ByteString read(InputStream in, int byteCount) throws IOException { - if (in == null) throw new IllegalArgumentException("in == null"); - if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); - - byte[] result = new byte[byteCount]; - for (int offset = 0, read; offset < byteCount; offset += read) { - read = in.read(result, offset, byteCount - offset); - if (read == -1) throw new EOFException(); - } - return new ByteString(result); - } - - /** - * Returns a byte string equal to this byte string, but with the bytes 'A' - * through 'Z' replaced with the corresponding byte in 'a' through 'z'. - * Returns this byte string if it contains no bytes in 'A' through 'Z'. - */ - public ByteString toAsciiLowercase() { - // Search for an uppercase character. If we don't find one, return this. - for (int i = 0; i < data.length; i++) { - byte c = data[i]; - if (c < 'A' || c > 'Z') continue; - - // If we reach this point, this string is not not lowercase. Create and - // return a new byte string. - byte[] lowercase = data.clone(); - lowercase[i++] = (byte) (c - ('A' - 'a')); - for (; i < lowercase.length; i++) { - c = lowercase[i]; - if (c < 'A' || c > 'Z') continue; - lowercase[i] = (byte) (c - ('A' - 'a')); - } - return new ByteString(lowercase); - } - return this; - } - - /** - * Returns a byte string equal to this byte string, but with the bytes 'a' - * through 'z' replaced with the corresponding byte in 'A' through 'Z'. - * Returns this byte string if it contains no bytes in 'a' through 'z'. - */ - public ByteString toAsciiUppercase() { - // Search for an lowercase character. If we don't find one, return this. - for (int i = 0; i < data.length; i++) { - byte c = data[i]; - if (c < 'a' || c > 'z') continue; - - // If we reach this point, this string is not not uppercase. Create and - // return a new byte string. - byte[] lowercase = data.clone(); - lowercase[i++] = (byte) (c - ('a' - 'A')); - for (; i < lowercase.length; i++) { - c = lowercase[i]; - if (c < 'a' || c > 'z') continue; - lowercase[i] = (byte) (c - ('a' - 'A')); - } - return new ByteString(lowercase); - } - return this; - } - - /** Returns the byte at {@code pos}. */ - public byte getByte(int pos) { - return data[pos]; - } - - /** - * Returns the number of bytes in this ByteString. - */ - public int size() { - return data.length; - } - - /** - * Returns a byte array containing a copy of the bytes in this {@code ByteString}. - */ - public byte[] toByteArray() { - return data.clone(); - } - - /** Writes the contents of this byte string to {@code out}. */ - public void write(OutputStream out) throws IOException { - if (out == null) throw new IllegalArgumentException("out == null"); - out.write(data); - } - - @Override public boolean equals(Object o) { - return o == this || o instanceof ByteString && Arrays.equals(((ByteString) o).data, data); - } - - @Override public int hashCode() { - int result = hashCode; - return result != 0 ? result : (hashCode = Arrays.hashCode(data)); - } - - @Override public String toString() { - if (data.length == 0) { - return "ByteString[size=0]"; - } - - if (data.length <= 16) { - return String.format("ByteString[size=%s data=%s]", data.length, hex()); - } - - try { - return String.format("ByteString[size=%s md5=%s]", data.length, - ByteString.of(MessageDigest.getInstance("MD5").digest(data)).hex()); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(); - } - } - - private void readObject(ObjectInputStream in) throws IOException { - int dataLength = in.readInt(); - ByteString byteString = ByteString.read(in, dataLength); - try { - Field field = ByteString.class.getDeclaredField("data"); - field.setAccessible(true); - field.set(this, byteString.data); - } catch (NoSuchFieldException e) { - throw new AssertionError(); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } - } - - private void writeObject(ObjectOutputStream out) throws IOException { - out.writeInt(data.length); - out.write(data); - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Okio.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Okio.java deleted file mode 100644 index 4aa579443..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Okio.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.util.logging.Level; -import java.util.logging.Logger; - -import static com.koushikdutta.async.http.spdy.okio.Util.checkOffsetAndCount; - -/** Essential APIs for working with Okio. */ -public final class Okio { - private static final Logger logger = Logger.getLogger(Okio.class.getName()); - - private Okio() { - } - - /** - * Returns a new source that buffers reads from {@code source}. The returned - * source will perform bulk reads into its in-memory buffer. Use this wherever - * you read a source to get an ergonomic and efficient access to data. - */ - public static BufferedSource buffer(Source source) { - if (source == null) throw new IllegalArgumentException("source == null"); - return new RealBufferedSource(source); - } - - /** - * Returns a new sink that buffers writes to {@code sink}. The returned sink - * will batch writes to {@code sink}. Use this wherever you write to a sink to - * get an ergonomic and efficient access to data. - */ - public static BufferedSink buffer(Sink sink) { - if (sink == null) throw new IllegalArgumentException("sink == null"); - return new RealBufferedSink(sink); - } - - /** Returns a sink that writes to {@code out}. */ - public static Sink sink(final OutputStream out) { - return sink(out, new Timeout()); - } - - private static Sink sink(final OutputStream out, final Timeout timeout) { - if (out == null) throw new IllegalArgumentException("out == null"); - if (timeout == null) throw new IllegalArgumentException("timeout == null"); - - return new Sink() { - @Override public void write(Buffer source, long byteCount) throws IOException { - checkOffsetAndCount(source.size, 0, byteCount); - while (byteCount > 0) { - timeout.throwIfReached(); - Segment head = source.head; - int toCopy = (int) Math.min(byteCount, head.limit - head.pos); - out.write(head.data, head.pos, toCopy); - - head.pos += toCopy; - byteCount -= toCopy; - source.size -= toCopy; - - if (head.pos == head.limit) { - source.head = head.pop(); - SegmentPool.getInstance().recycle(head); - } - } - } - - @Override public void flush() throws IOException { - out.flush(); - } - - @Override public void close() throws IOException { - out.close(); - } - - @Override public Timeout timeout() { - return timeout; - } - - @Override public String toString() { - return "sink(" + out + ")"; - } - }; - } - - /** - * Returns a sink that writes to {@code socket}. Prefer this over {@link - * #sink(java.io.OutputStream)} because this method honors timeouts. When the socket - * write times out, the socket is asynchronously closed by a watchdog thread. - */ - public static Sink sink(final Socket socket) throws IOException { - if (socket == null) throw new IllegalArgumentException("socket == null"); - AsyncTimeout timeout = timeout(socket); - Sink sink = sink(socket.getOutputStream(), timeout); - return timeout.sink(sink); - } - - /** Returns a source that reads from {@code in}. */ - public static Source source(final InputStream in) { - return source(in, new Timeout()); - } - - private static Source source(final InputStream in, final Timeout timeout) { - if (in == null) throw new IllegalArgumentException("in == null"); - if (timeout == null) throw new IllegalArgumentException("timeout == null"); - - return new Source() { - @Override public long read(Buffer sink, long byteCount) throws IOException { - if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); - timeout.throwIfReached(); - Segment tail = sink.writableSegment(1); - int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit); - int bytesRead = in.read(tail.data, tail.limit, maxToCopy); - if (bytesRead == -1) return -1; - tail.limit += bytesRead; - sink.size += bytesRead; - return bytesRead; - } - - @Override public void close() throws IOException { - in.close(); - } - - @Override public Timeout timeout() { - return timeout; - } - - @Override public String toString() { - return "source(" + in + ")"; - } - }; - } - - /** Returns a source that reads from {@code file}. */ - public static Source source(File file) throws FileNotFoundException { - if (file == null) throw new IllegalArgumentException("file == null"); - return source(new FileInputStream(file)); - } - - /** Returns a sink that writes to {@code file}. */ - public static Sink sink(File file) throws FileNotFoundException { - if (file == null) throw new IllegalArgumentException("file == null"); - return sink(new FileOutputStream(file)); - } - - /** Returns a sink that appends to {@code file}. */ - public static Sink appendingSink(File file) throws FileNotFoundException { - if (file == null) throw new IllegalArgumentException("file == null"); - return sink(new FileOutputStream(file, true)); - } - - /** - * Returns a source that reads from {@code socket}. Prefer this over {@link - * #source(java.io.InputStream)} because this method honors timeouts. When the socket - * read times out, the socket is asynchronously closed by a watchdog thread. - */ - public static Source source(final Socket socket) throws IOException { - if (socket == null) throw new IllegalArgumentException("socket == null"); - AsyncTimeout timeout = timeout(socket); - Source source = source(socket.getInputStream(), timeout); - return timeout.source(source); - } - - private static AsyncTimeout timeout(final Socket socket) { - return new AsyncTimeout() { - @Override protected void timedOut() { - try { - socket.close(); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e); - } - } - }; - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSink.java deleted file mode 100644 index 8e393ca42..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSink.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.Charset; - -final class RealBufferedSink implements BufferedSink { - public final Buffer buffer; - public final Sink sink; - private boolean closed; - - public RealBufferedSink(Sink sink, Buffer buffer) { - if (sink == null) throw new IllegalArgumentException("sink == null"); - this.buffer = buffer; - this.sink = sink; - } - - public RealBufferedSink(Sink sink) { - this(sink, new Buffer()); - } - - @Override public Buffer buffer() { - return buffer; - } - - @Override public void write(Buffer source, long byteCount) - throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.write(source, byteCount); - emitCompleteSegments(); - } - - @Override public BufferedSink write(ByteString byteString) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.write(byteString); - return emitCompleteSegments(); - } - - @Override public BufferedSink writeUtf8(String string) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.writeUtf8(string); - return emitCompleteSegments(); - } - - @Override public BufferedSink writeString(String string, Charset charset) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.writeString(string, charset); - return emitCompleteSegments(); - } - - @Override public BufferedSink write(byte[] source) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.write(source); - return emitCompleteSegments(); - } - - @Override public BufferedSink write(byte[] source, int offset, int byteCount) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.write(source, offset, byteCount); - return emitCompleteSegments(); - } - - @Override public long writeAll(Source source) throws IOException { - if (source == null) throw new IllegalArgumentException("source == null"); - long totalBytesRead = 0; - for (long readCount; (readCount = source.read(buffer, Segment.SIZE)) != -1; ) { - totalBytesRead += readCount; - emitCompleteSegments(); - } - return totalBytesRead; - } - - @Override public BufferedSink writeByte(int b) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.writeByte(b); - return emitCompleteSegments(); - } - - @Override public BufferedSink writeShort(int s) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.writeShort(s); - return emitCompleteSegments(); - } - - @Override public BufferedSink writeShortLe(int s) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.writeShortLe(s); - return emitCompleteSegments(); - } - - @Override public BufferedSink writeInt(int i) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.writeInt(i); - return emitCompleteSegments(); - } - - @Override public BufferedSink writeIntLe(int i) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.writeIntLe(i); - return emitCompleteSegments(); - } - - @Override public BufferedSink writeLong(long v) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.writeLong(v); - return emitCompleteSegments(); - } - - @Override public BufferedSink writeLongLe(long v) throws IOException { - if (closed) throw new IllegalStateException("closed"); - buffer.writeLongLe(v); - return emitCompleteSegments(); - } - - @Override public BufferedSink emitCompleteSegments() throws IOException { - if (closed) throw new IllegalStateException("closed"); - long byteCount = buffer.completeSegmentByteCount(); - if (byteCount > 0) sink.write(buffer, byteCount); - return this; - } - - @Override public OutputStream outputStream() { - return new OutputStream() { - @Override public void write(int b) throws IOException { - if (closed) throw new IOException("closed"); - buffer.writeByte((byte) b); - emitCompleteSegments(); - } - - @Override public void write(byte[] data, int offset, int byteCount) throws IOException { - if (closed) throw new IOException("closed"); - buffer.write(data, offset, byteCount); - emitCompleteSegments(); - } - - @Override public void flush() throws IOException { - // For backwards compatibility, a flush() on a closed stream is a no-op. - if (!closed) { - RealBufferedSink.this.flush(); - } - } - - @Override public void close() throws IOException { - RealBufferedSink.this.close(); - } - - @Override public String toString() { - return RealBufferedSink.this + ".outputStream()"; - } - }; - } - - @Override public void flush() throws IOException { - if (closed) throw new IllegalStateException("closed"); - if (buffer.size > 0) { - sink.write(buffer, buffer.size); - } - sink.flush(); - } - - @Override public void close() throws IOException { - if (closed) return; - - // Emit buffered data to the underlying sink. If this fails, we still need - // to close the sink; otherwise we risk leaking resources. - Throwable thrown = null; - try { - if (buffer.size > 0) { - sink.write(buffer, buffer.size); - } - } catch (Throwable e) { - thrown = e; - } - - try { - sink.close(); - } catch (Throwable e) { - if (thrown == null) thrown = e; - } - closed = true; - - if (thrown != null) Util.sneakyRethrow(thrown); - } - - @Override public Timeout timeout() { - return sink.timeout(); - } - - @Override public String toString() { - return "buffer(" + sink + ")"; - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSource.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSource.java deleted file mode 100644 index 0397efdd0..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/RealBufferedSource.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; - -import static com.koushikdutta.async.http.spdy.okio.Util.checkOffsetAndCount; - -final class RealBufferedSource implements BufferedSource { - public final Buffer buffer; - public final Source source; - private boolean closed; - - public RealBufferedSource(Source source, Buffer buffer) { - if (source == null) throw new IllegalArgumentException("source == null"); - this.buffer = buffer; - this.source = source; - } - - public RealBufferedSource(Source source) { - this(source, new Buffer()); - } - - @Override public Buffer buffer() { - return buffer; - } - - @Override public long read(Buffer sink, long byteCount) throws IOException { - if (sink == null) throw new IllegalArgumentException("sink == null"); - if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); - if (closed) throw new IllegalStateException("closed"); - - if (buffer.size == 0) { - long read = source.read(buffer, Segment.SIZE); - if (read == -1) return -1; - } - - long toRead = Math.min(byteCount, buffer.size); - return buffer.read(sink, toRead); - } - - @Override public boolean exhausted() throws IOException { - if (closed) throw new IllegalStateException("closed"); - return buffer.exhausted() && source.read(buffer, Segment.SIZE) == -1; - } - - @Override public void require(long byteCount) throws IOException { - if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); - if (closed) throw new IllegalStateException("closed"); - while (buffer.size < byteCount) { - if (source.read(buffer, Segment.SIZE) == -1) throw new EOFException(); - } - } - - @Override public byte readByte() throws IOException { - require(1); - return buffer.readByte(); - } - - @Override public ByteString readByteString() throws IOException { - buffer.writeAll(source); - return buffer.readByteString(); - } - - @Override public ByteString readByteString(long byteCount) throws IOException { - require(byteCount); - return buffer.readByteString(byteCount); - } - - @Override public byte[] readByteArray() throws IOException { - buffer.writeAll(source); - return buffer.readByteArray(); - } - - @Override public byte[] readByteArray(long byteCount) throws IOException { - require(byteCount); - return buffer.readByteArray(byteCount); - } - - @Override public int read(byte[] sink) throws IOException { - return read(sink, 0, sink.length); - } - - @Override public void readFully(byte[] sink) throws IOException { - try { - require(sink.length); - } catch (EOFException e) { - // The underlying source is exhausted. Copy the bytes we got before rethrowing. - int offset = 0; - while (buffer.size > 0) { - int read = buffer.read(sink, offset, (int) buffer.size - offset); - if (read == -1) throw new AssertionError(); - offset += read; - } - throw e; - } - buffer.readFully(sink); - } - - @Override public int read(byte[] sink, int offset, int byteCount) throws IOException { - checkOffsetAndCount(sink.length, offset, byteCount); - - if (buffer.size == 0) { - long read = source.read(buffer, Segment.SIZE); - if (read == -1) return -1; - } - - int toRead = (int) Math.min(byteCount, buffer.size); - return buffer.read(sink, offset, toRead); - } - - @Override public void readFully(Buffer sink, long byteCount) throws IOException { - try { - require(byteCount); - } catch (EOFException e) { - // The underlying source is exhausted. Copy the bytes we got before rethrowing. - sink.writeAll(buffer); - throw e; - } - buffer.readFully(sink, byteCount); - } - - @Override public long readAll(Sink sink) throws IOException { - if (sink == null) throw new IllegalArgumentException("sink == null"); - - long totalBytesWritten = 0; - while (source.read(buffer, Segment.SIZE) != -1) { - long emitByteCount = buffer.completeSegmentByteCount(); - if (emitByteCount > 0) { - totalBytesWritten += emitByteCount; - sink.write(buffer, emitByteCount); - } - } - if (buffer.size() > 0) { - totalBytesWritten += buffer.size(); - sink.write(buffer, buffer.size()); - } - return totalBytesWritten; - } - - @Override public String readUtf8() throws IOException { - buffer.writeAll(source); - return buffer.readUtf8(); - } - - @Override public String readUtf8(long byteCount) throws IOException { - require(byteCount); - return buffer.readUtf8(byteCount); - } - - @Override public String readString(Charset charset) throws IOException { - if (charset == null) throw new IllegalArgumentException("charset == null"); - - buffer.writeAll(source); - return buffer.readString(charset); - } - - @Override public String readString(long byteCount, Charset charset) throws IOException { - require(byteCount); - if (charset == null) throw new IllegalArgumentException("charset == null"); - return buffer.readString(byteCount, charset); - } - - @Override public String readUtf8Line() throws IOException { - long newline = indexOf((byte) '\n'); - - if (newline == -1) { - return buffer.size != 0 ? readUtf8(buffer.size) : null; - } - - return buffer.readUtf8Line(newline); - } - - @Override public String readUtf8LineStrict() throws IOException { - long newline = indexOf((byte) '\n'); - if (newline == -1L) throw new EOFException(); - return buffer.readUtf8Line(newline); - } - - @Override public short readShort() throws IOException { - require(2); - return buffer.readShort(); - } - - @Override public short readShortLe() throws IOException { - require(2); - return buffer.readShortLe(); - } - - @Override public int readInt() throws IOException { - require(4); - return buffer.readInt(); - } - - @Override public int readIntLe() throws IOException { - require(4); - return buffer.readIntLe(); - } - - @Override public long readLong() throws IOException { - require(8); - return buffer.readLong(); - } - - @Override public long readLongLe() throws IOException { - require(8); - return buffer.readLongLe(); - } - - @Override public void skip(long byteCount) throws IOException { - if (closed) throw new IllegalStateException("closed"); - while (byteCount > 0) { - if (buffer.size == 0 && source.read(buffer, Segment.SIZE) == -1) { - throw new EOFException(); - } - long toSkip = Math.min(byteCount, buffer.size()); - buffer.skip(toSkip); - byteCount -= toSkip; - } - } - - @Override public long indexOf(byte b) throws IOException { - if (closed) throw new IllegalStateException("closed"); - long start = 0; - long index; - while ((index = buffer.indexOf(b, start)) == -1) { - start = buffer.size; - if (source.read(buffer, Segment.SIZE) == -1) return -1L; - } - return index; - } - - @Override public InputStream inputStream() { - return new InputStream() { - @Override public int read() throws IOException { - if (closed) throw new IOException("closed"); - if (buffer.size == 0) { - long count = source.read(buffer, Segment.SIZE); - if (count == -1) return -1; - } - return buffer.readByte() & 0xff; - } - - @Override public int read(byte[] data, int offset, int byteCount) throws IOException { - if (closed) throw new IOException("closed"); - checkOffsetAndCount(data.length, offset, byteCount); - - if (buffer.size == 0) { - long count = source.read(buffer, Segment.SIZE); - if (count == -1) return -1; - } - - return buffer.read(data, offset, byteCount); - } - - @Override public int available() throws IOException { - if (closed) throw new IOException("closed"); - return (int) Math.min(buffer.size, Integer.MAX_VALUE); - } - - @Override public void close() throws IOException { - RealBufferedSource.this.close(); - } - - @Override public String toString() { - return RealBufferedSource.this + ".inputStream()"; - } - }; - } - - @Override public void close() throws IOException { - if (closed) return; - closed = true; - source.close(); - buffer.clear(); - } - - @Override public Timeout timeout() { - return source.timeout(); - } - - @Override public String toString() { - return "buffer(" + source + ")"; - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java deleted file mode 100644 index 9c289ef41..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Segment.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -/** - * A segment of a buffer. - * - *

Each segment in a buffer is a circularly-linked list node referencing - * the following and preceding segments in the buffer. - * - *

Each segment in the pool is a singly-linked list node referencing the rest - * of segments in the pool. - */ -public final class Segment { - /** The size of all segments in bytes. */ - // TODO: Using fixed-size segments makes pooling easier. But it harms memory - // efficiency and encourages copying. Try variable sized segments? - // TODO: Is 2 KiB a good default segment size? - static final int SIZE = 2048; - - public final byte[] data = new byte[SIZE]; - - /** The next byte of application data byte to read in this segment. */ - public int pos; - - /** The first byte of available data ready to be written to. */ - public int limit; - - /** Next segment in a linked or circularly-linked list. */ - Segment next; - - /** Previous segment in a circularly-linked list. */ - Segment prev; - - /** - * Removes this segment of a circularly-linked list and returns its successor. - * Returns null if the list is now empty. - */ - public Segment pop() { - Segment result = next != this ? next : null; - prev.next = next; - next.prev = prev; - next = null; - prev = null; - return result; - } - - /** - * Appends {@code segment} after this segment in the circularly-linked list. - * Returns the pushed segment. - */ - public Segment push(Segment segment) { - segment.prev = this; - segment.next = next; - next.prev = segment; - next = segment; - return segment; - } - - /** - * Splits this head of a circularly-linked list into two segments. The first - * segment contains the data in {@code [pos..pos+byteCount)}. The second - * segment contains the data in {@code [pos+byteCount..limit)}. This can be - * useful when moving partial segments from one buffer to another. - * - *

Returns the new head of the circularly-linked list. - */ - public Segment split(int byteCount) { - int aSize = byteCount; - int bSize = (limit - pos) - byteCount; - if (aSize <= 0 || bSize <= 0) throw new IllegalArgumentException(); - - // Which side of the split is larger? We want to copy as few bytes as possible. - if (aSize < bSize) { - // Create a segment of size 'aSize' before this segment. - Segment before = SegmentPool.getInstance().take(); - System.arraycopy(data, pos, before.data, before.pos, aSize); - pos += aSize; - before.limit += aSize; - prev.push(before); - return before; - } else { - // Create a new segment of size 'bSize' after this segment. - Segment after = SegmentPool.getInstance().take(); - System.arraycopy(data, pos + aSize, after.data, after.pos, bSize); - limit -= bSize; - after.limit += bSize; - push(after); - return this; - } - } - - /** - * Call this when the tail and its predecessor may both be less than half - * full. This will copy data so that segments can be recycled. - */ - public void compact() { - if (prev == this) throw new IllegalStateException(); - if ((prev.limit - prev.pos) + (limit - pos) > SIZE) return; // Cannot compact. - writeTo(prev, limit - pos); - pop(); - SegmentPool.getInstance().recycle(this); - } - - /** Moves {@code byteCount} bytes from this segment to {@code sink}. */ - // TODO: if sink has fewer bytes than this, it may be cheaper to reverse the - // direction of the copy and swap the segments! - public void writeTo(Segment sink, int byteCount) { - if (byteCount + (sink.limit - sink.pos) > SIZE) throw new IllegalArgumentException(); - - if (sink.limit + byteCount > SIZE) { - // We can't fit byteCount bytes at the sink's current position. Compact sink first. - System.arraycopy(sink.data, sink.pos, sink.data, 0, sink.limit - sink.pos); - sink.limit -= sink.pos; - sink.pos = 0; - } - - System.arraycopy(data, pos, sink.data, sink.limit, byteCount); - sink.limit += byteCount; - pos += byteCount; - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java deleted file mode 100644 index 58d362b90..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/SegmentPool.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -/** - * A collection of unused segments, necessary to avoid GC churn and zero-fill. - * This pool is a thread-safe static singleton. - */ -public final class SegmentPool { - private static final SegmentPool INSTANCE = new SegmentPool(); - public static SegmentPool getInstance() { - return INSTANCE; - } - - /** The maximum number of bytes to pool. */ - // TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments? - static final long MAX_SIZE = 64 * 1024; // 64 KiB. - - /** Singly-linked list of segments. */ - private Segment next; - - /** Total bytes in this pool. */ - long byteCount; - - private SegmentPool() { - } - - Segment take() { - synchronized (this) { - if (next != null) { - Segment result = next; - next = result.next; - result.next = null; - byteCount -= Segment.SIZE; - return result; - } - } - return new Segment(); // Pool is empty. Don't zero-fill while holding a lock. - } - - public void recycle(Segment segment) { - if (segment.next != null || segment.prev != null) throw new IllegalArgumentException(); - synchronized (this) { - if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full. - byteCount += Segment.SIZE; - segment.next = next; - segment.pos = segment.limit = 0; - next = segment; - } - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Sink.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Sink.java deleted file mode 100644 index d1e3cc626..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Sink.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.Closeable; -import java.io.IOException; - -/** - * Receives a stream of bytes. Use this interface to write data wherever it's - * needed: to the network, storage, or a buffer in memory. Sinks may be layered - * to transform received data, such as to compress, encrypt, throttle, or add - * protocol framing. - * - *

Most application code shouldn't operate on a sink directly, but rather - * {@link BufferedSink} which is both more efficient and more convenient. Use - * {@link Okio#buffer(com.koushikdutta.async.http.spdy.okio.Sink)} to wrap any sink with a buffer. - * - *

Sinks are easy to test: just use an {@link Buffer} in your tests, and - * read from it to confirm it received the data that was expected. - * - *

Comparison with OutputStream

- * This interface is functionally equivalent to {@link java.io.OutputStream}. - * - *

{@code OutputStream} requires multiple layers when emitted data is - * heterogeneous: a {@code DataOutputStream} for primitive values, a {@code - * BufferedOutputStream} for buffering, and {@code OutputStreamWriter} for - * charset encoding. This class uses {@code BufferedSink} for all of the above. - * - *

Sink is also easier to layer: there is no {@linkplain - * java.io.OutputStream#write(int) single-byte write} method that is awkward to - * implement efficiently. - * - *

Interop with OutputStream

- * Use {@link Okio#sink} to adapt an {@code OutputStream} to a sink. Use {@link - * BufferedSink#outputStream} to adapt a sink to an {@code OutputStream}. - */ -public interface Sink extends Closeable { - /** Removes {@code byteCount} bytes from {@code source} and appends them to this. */ - void write(Buffer source, long byteCount) throws IOException; - - /** Pushes all buffered bytes to their final destination. */ - void flush() throws IOException; - - /** Returns the timeout for this sink. */ - Timeout timeout(); - - /** - * Pushes all buffered bytes to their final destination and releases the - * resources held by this sink. It is an error to write a closed sink. It is - * safe to close a sink more than once. - */ - @Override void close() throws IOException; -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Source.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Source.java deleted file mode 100644 index 4f4132252..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Source.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.Closeable; -import java.io.IOException; - -/** - * Supplies a stream of bytes. Use this interface to read data from wherever - * it's located: from the network, storage, or a buffer in memory. Sources may - * be layered to transform supplied data, such as to decompress, decrypt, or - * remove protocol framing. - * - *

Most applications shouldn't operate on a source directly, but rather - * {@link BufferedSource} which is both more efficient and more convenient. Use - * {@link Okio#buffer(com.koushikdutta.async.http.spdy.okio.Source)} to wrap any source with a buffer. - * - *

Sources are easy to test: just use an {@link Buffer} in your tests, and - * fill it with the data your application is to read. - * - *

Comparison with InputStream

- * This interface is functionally equivalent to {@link java.io.InputStream}. - * - *

{@code InputStream} requires multiple layers when consumed data is - * heterogeneous: a {@code DataInputStream} for primitive values, a {@code - * BufferedInputStream} for buffering, and {@code InputStreamReader} for - * strings. This class uses {@code BufferedSource} for all of the above. - * - *

Source avoids the impossible-to-implement {@linkplain - * java.io.InputStream#available available()} method. Instead callers specify - * how many bytes they {@link BufferedSource#require require}. - * - *

Source omits the unsafe-to-compose {@linkplain java.io.InputStream#mark - * mark and reset} state that's tracked by {@code InputStream}; callers instead - * just buffer what they need. - * - *

When implementing a source, you need not worry about the {@linkplain - * java.io.InputStream#read single-byte read} method that is awkward to - * implement efficiently and that returns one of 257 possible values. - * - *

And source has a stronger {@code skip} method: {@link BufferedSource#skip} - * won't return prematurely. - * - *

Interop with InputStream

- * Use {@link Okio#source} to adapt an {@code InputStream} to a source. Use - * {@link BufferedSource#inputStream} to adapt a source to an {@code - * InputStream}. - */ -public interface Source extends Closeable { - /** - * Removes at least 1, and up to {@code byteCount} bytes from this and appends - * them to {@code sink}. Returns the number of bytes read, or -1 if this - * source is exhausted. - */ - long read(Buffer sink, long byteCount) throws IOException; - - /** Returns the timeout for this source. */ - Timeout timeout(); - - /** - * Closes this source and releases the resources held by this source. It is an - * error to read a closed source. It is safe to close a source more than once. - */ - @Override void close() throws IOException; -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Timeout.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Timeout.java deleted file mode 100644 index 3a307e8d7..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Timeout.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.io.IOException; -import java.io.InterruptedIOException; -import java.util.concurrent.TimeUnit; - -/** - * A policy on how much time to spend on a task before giving up. When a task - * times out, it is left in an unspecified state and should be abandoned. For - * example, if reading from a source times out, that source should be closed and - * the read should be retried later. If writing to a sink times out, the same - * rules apply: close the sink and retry later. - * - *

Timeouts and Deadlines

- * This class offers two complementary controls to define a timeout policy. - * - *

Timeouts specify the maximum time to wait for a single - * operation to complete. Timeouts are typically used to detect problems like - * network partitions. For example, if a remote peer doesn't return any - * data for ten seconds, we may assume that the peer is unavailable. - * - *

Deadlines specify the maximum time to spend on a job, - * composed of one or more operations. Use deadlines to set an upper bound on - * the time invested on a job. For example, a battery-conscious app may limit - * how much time it spends preloading content. - */ -public class Timeout { - /** - * An empty timeout that neither tracks nor detects timeouts. Use this when - * timeouts aren't necessary, such as in implementations whose operations - * do not block. - */ - public static final Timeout NONE = new Timeout() { - @Override public Timeout timeout(long timeout, TimeUnit unit) { - return this; - } - - @Override public Timeout deadlineNanoTime(long deadlineNanoTime) { - return this; - } - - @Override public void throwIfReached() throws IOException { - } - }; - - /** - * True if {@code deadlineNanoTime} is defined. There is no equivalent to null - * or 0 for {@link System#nanoTime}. - */ - private boolean hasDeadline; - private long deadlineNanoTime; - private long timeoutNanos; - - public Timeout() { - } - - /** - * Wait at most {@code timeout} time before aborting an operation. Using a - * per-operation timeout means that as long as forward progress is being made, - * no sequence of operations will fail. - * - *

If {@code timeout == 0}, operations will run indefinitely. (Operating - * system timeouts may still apply.) - */ - public Timeout timeout(long timeout, TimeUnit unit) { - if (timeout < 0) throw new IllegalArgumentException("timeout < 0: " + timeout); - if (unit == null) throw new IllegalArgumentException("unit == null"); - this.timeoutNanos = unit.toNanos(timeout); - return this; - } - - /** Returns the timeout in nanoseconds, or {@code 0} for no timeout. */ - public long timeoutNanos() { - return timeoutNanos; - } - - /** Returns true if a deadline is enabled. */ - public boolean hasDeadline() { - return hasDeadline; - } - - /** - * Returns the {@linkplain System#nanoTime() nano time} when the deadline will - * be reached. - * - * @throws IllegalStateException if no deadline is set. - */ - public long deadlineNanoTime() { - if (!hasDeadline) throw new IllegalStateException("No deadline"); - return deadlineNanoTime; - } - - /** - * Sets the {@linkplain System#nanoTime() nano time} when the deadline will be - * reached. All operations must complete before this time. Use a deadline to - * set a maximum bound on the time spent on a sequence of operations. - */ - public Timeout deadlineNanoTime(long deadlineNanoTime) { - this.hasDeadline = true; - this.deadlineNanoTime = deadlineNanoTime; - return this; - } - - /** Set a deadline of now plus {@code duration} time. */ - public final Timeout deadline(long duration, TimeUnit unit) { - if (duration <= 0) throw new IllegalArgumentException("duration <= 0: " + duration); - if (unit == null) throw new IllegalArgumentException("unit == null"); - return deadlineNanoTime(System.nanoTime() + unit.toNanos(duration)); - } - - /** Clears the timeout. Operating system timeouts may still apply. */ - public Timeout clearTimeout() { - this.timeoutNanos = 0; - return this; - } - - /** Clears the deadline. */ - public Timeout clearDeadline() { - this.hasDeadline = false; - return this; - } - - /** - * Throws an {@link java.io.IOException} if the deadline has been reached or if the - * current thread has been interrupted. This method doesn't detect timeouts; - * that should be implemented to asynchronously abort an in-progress - * operation. - */ - public void throwIfReached() throws IOException { - if (Thread.interrupted()) { - throw new InterruptedIOException(); - } - - if (hasDeadline && System.nanoTime() > deadlineNanoTime) { - throw new IOException("deadline reached"); - } - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Util.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Util.java deleted file mode 100644 index 14775e85a..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okio/Util.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2014 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.koushikdutta.async.http.spdy.okio; - -import java.nio.charset.Charset; - -final class Util { - /** A cheap and type-safe constant for the UTF-8 Charset. */ - public static final Charset UTF_8 = Charset.forName("UTF-8"); - - private Util() { - } - - public static void checkOffsetAndCount(long size, long offset, long byteCount) { - if ((offset | byteCount) < 0 || offset > size || size - offset < byteCount) { - throw new ArrayIndexOutOfBoundsException( - String.format("size=%s offset=%s byteCount=%s", size, offset, byteCount)); - } - } - - public static short reverseBytesShort(short s) { - int i = s & 0xffff; - int reversed = (i & 0xff00) >>> 8 - | (i & 0x00ff) << 8; - return (short) reversed; - } - - public static int reverseBytesInt(int i) { - return (i & 0xff000000) >>> 24 - | (i & 0x00ff0000) >>> 8 - | (i & 0x0000ff00) << 8 - | (i & 0x000000ff) << 24; - } - - public static long reverseBytesLong(long v) { - return (v & 0xff00000000000000L) >>> 56 - | (v & 0x00ff000000000000L) >>> 40 - | (v & 0x0000ff0000000000L) >>> 24 - | (v & 0x000000ff00000000L) >>> 8 - | (v & 0x00000000ff000000L) << 8 - | (v & 0x0000000000ff0000L) << 24 - | (v & 0x000000000000ff00L) << 40 - | (v & 0x00000000000000ffL) << 56; - } - - /** - * Throws {@code t}, even if the declared throws clause doesn't permit it. - * This is a terrible – but terribly convenient – hack that makes it easy to - * catch and rethrow exceptions after cleanup. See Java Puzzlers #43. - */ - public static void sneakyRethrow(Throwable t) { - Util.sneakyThrow2(t); - } - - @SuppressWarnings("unchecked") - private static void sneakyThrow2(Throwable t) throws T { - throw (T) t; - } -} From 0e28ad9a43f799df307e7f0972d48e4c1108b4b9 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 13:53:02 -0700 Subject: [PATCH 058/399] more okhttp reshuffling and purging --- .../async/http/spdy/okhttp/internal/Util.java | 25 ------------------- .../koushikdutta/async/test}/Handshake.java | 2 +- .../koushikdutta/async/test/OkHttpTest.java | 1 - 3 files changed, 1 insertion(+), 27 deletions(-) rename AndroidAsync/{src/com/koushikdutta/async/http/spdy/okhttp => test/src/com/koushikdutta/async/test}/Handshake.java (98%) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java index 63f2661aa..6c8c0e4f1 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java @@ -16,8 +16,6 @@ package com.koushikdutta.async.http.spdy.okhttp.internal; -import java.io.Closeable; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -31,29 +29,6 @@ public static void checkOffsetAndCount(long arrayLength, long offset, long count } } - /** - * Closes {@code a} and {@code b}. If either close fails, this completes - * the other close and rethrows the first encountered exception. - */ - public static void closeAll(Closeable a, Closeable b) throws IOException { - Throwable thrown = null; - try { - a.close(); - } catch (Throwable e) { - thrown = e; - } - try { - b.close(); - } catch (Throwable e) { - if (thrown == null) thrown = e; - } - if (thrown == null) return; - if (thrown instanceof IOException) throw (IOException) thrown; - if (thrown instanceof RuntimeException) throw (RuntimeException) thrown; - if (thrown instanceof Error) throw (Error) thrown; - throw new AssertionError(thrown); - } - /** Returns an immutable copy of {@code list}. */ public static List immutableList(List list) { return Collections.unmodifiableList(new ArrayList(list)); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/Handshake.java b/AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java similarity index 98% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/Handshake.java rename to AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java index b9ae5de7c..e53908f72 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/Handshake.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java @@ -1,4 +1,4 @@ -package com.koushikdutta.async.http.spdy.okhttp; +package com.koushikdutta.async.test; import com.koushikdutta.async.http.spdy.okhttp.internal.Util; diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java b/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java index 821de08fd..48fbf5b69 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java @@ -4,7 +4,6 @@ import android.test.AndroidTestCase; import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okhttp.Handshake; import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.util.Charsets; From 9ec1a8ded7e3b59da7c2b53ebe0e4cf4ece566e1 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 13:56:37 -0700 Subject: [PATCH 059/399] Restructure into single package, change visibility to package only. --- .../async/http/spdy/AsyncSpdyConnection.java | 13 +- .../spdy/{okhttp/internal => }/BitArray.java | 4 +- .../async/http/spdy/ByteString.java | 285 +++++++ .../{okhttp/internal/spdy => }/ErrorCode.java | 4 +- .../internal/spdy => }/FrameReader.java | 5 +- .../internal/spdy => }/FrameWriter.java | 4 +- .../{okhttp/internal/spdy => }/Header.java | 6 +- .../internal/spdy => }/HeaderReader.java | 5 +- .../internal/spdy => }/HeadersMode.java | 2 +- .../internal/spdy => }/HpackDraft08.java | 4 +- .../async/http/spdy/Http20Draft13.java | 764 ++++++++++++++++++ .../{okhttp/internal/spdy => }/Huffman.java | 2 +- .../spdy/{okhttp/internal/spdy => }/Ping.java | 4 +- .../{okhttp/internal/spdy => }/Settings.java | 4 +- .../{okhttp/internal/spdy => }/Spdy3.java | 5 +- .../async/http/spdy/SpdyMiddleware.java | 1 - .../async/http/spdy/SpdyTransport.java | 1 - .../http/spdy/{okhttp/internal => }/Util.java | 4 +- .../{okhttp/internal/spdy => }/Variant.java | 4 +- .../async/test/ConscryptTests.java | 12 +- .../koushikdutta/async/test/Handshake.java | 2 +- 21 files changed, 1082 insertions(+), 53 deletions(-) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal => }/BitArray.java (98%) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteString.java rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/ErrorCode.java (96%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/FrameReader.java (97%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/FrameWriter.java (97%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/Header.java (92%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/HeaderReader.java (92%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/HeadersMode.java (95%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/HpackDraft08.java (99%) create mode 100644 AndroidAsync/src/com/koushikdutta/async/http/spdy/Http20Draft13.java rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/Huffman.java (99%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/Ping.java (95%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/Settings.java (98%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/Spdy3.java (99%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal => }/Util.java (94%) rename AndroidAsync/src/com/koushikdutta/async/http/spdy/{okhttp/internal/spdy => }/Variant.java (93%) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index 45117dc4a..aca08f28d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -10,17 +10,6 @@ import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.future.SimpleFuture; import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.ErrorCode; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameReader; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameWriter; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.HeadersMode; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Http20Draft13; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Ping; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Variant; import java.io.IOException; import java.util.Hashtable; @@ -28,7 +17,7 @@ import java.util.List; import java.util.Map; -import static com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE; +import static com.koushikdutta.async.http.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE; /** * Created by koush on 7/16/14. diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/BitArray.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/BitArray.java similarity index 98% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/BitArray.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/BitArray.java index 5db2b6e20..1aa55b0dc 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/BitArray.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/BitArray.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal; +package com.koushikdutta.async.http.spdy; import java.util.ArrayList; import java.util.Arrays; @@ -22,7 +22,7 @@ import static java.lang.String.format; /** A simple bitset which supports left shifting. */ -public interface BitArray { +interface BitArray { void clear(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteString.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteString.java new file mode 100644 index 000000000..263b41bc0 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ByteString.java @@ -0,0 +1,285 @@ +/* + * Copyright 2014 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.koushikdutta.async.http.spdy; + +import android.util.Base64; + +import com.koushikdutta.async.util.Charsets; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +/** + * An immutable sequence of bytes. + * + *

Full disclosure: this class provides untrusted input and + * output streams with raw access to the underlying byte array. A hostile + * stream implementation could keep a reference to the mutable byte string, + * violating the immutable guarantee of this class. For this reason a byte + * string's immutability guarantee cannot be relied upon for security in applets + * and other environments that run both trusted and untrusted code in the same + * process. + */ +final class ByteString implements Serializable { + private static final char[] HEX_DIGITS = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + private static final long serialVersionUID = 1L; + + /** A singleton empty {@code ByteString}. */ + public static final ByteString EMPTY = ByteString.of(); + + final byte[] data; + private transient int hashCode; // Lazily computed; 0 if unknown. + private transient String utf8; // Lazily computed. + + ByteString(byte[] data) { + this.data = data; // Trusted internal constructor doesn't clone data. + } + + /** + * Returns a new byte string containing a clone of the bytes of {@code data}. + */ + public static ByteString of(byte... data) { + if (data == null) throw new IllegalArgumentException("data == null"); + return new ByteString(data.clone()); + } + + /** + * Returns a new byte string containing a copy of {@code byteCount} bytes of {@code data} starting + * at {@code offset}. + */ + public static ByteString of(byte[] data, int offset, int byteCount) { + if (data == null) throw new IllegalArgumentException("data == null"); + Util.checkOffsetAndCount(data.length, offset, byteCount); + + byte[] copy = new byte[byteCount]; + System.arraycopy(data, offset, copy, 0, byteCount); + return new ByteString(copy); + } + + /** Returns a new byte string containing the {@code UTF-8} bytes of {@code s}. */ + public static ByteString encodeUtf8(String s) { + if (s == null) throw new IllegalArgumentException("s == null"); + ByteString byteString = new ByteString(s.getBytes(Charsets.UTF_8)); + byteString.utf8 = s; + return byteString; + } + + /** Constructs a new {@code String} by decoding the bytes as {@code UTF-8}. */ + public String utf8() { + String result = utf8; + // We don't care if we double-allocate in racy code. + return result != null ? result : (utf8 = new String(data, Charsets.UTF_8)); + } + + /** + * Returns this byte string encoded as Base64. In violation of the + * RFC, the returned string does not wrap lines at 76 columns. + */ + public String base64() { + return Base64.encodeToString(data, Base64.DEFAULT); + } + + /** + * Decodes the Base64-encoded bytes and returns their value as a byte string. + * Returns null if {@code base64} is not a Base64-encoded sequence of bytes. + */ + public static ByteString decodeBase64(String base64) { + if (base64 == null) throw new IllegalArgumentException("base64 == null"); + byte[] decoded = Base64.decode(base64, Base64.DEFAULT); + return decoded != null ? new ByteString(decoded) : null; + } + + /** Returns this byte string encoded in hexadecimal. */ + public String hex() { + char[] result = new char[data.length * 2]; + int c = 0; + for (byte b : data) { + result[c++] = HEX_DIGITS[(b >> 4) & 0xf]; + result[c++] = HEX_DIGITS[b & 0xf]; + } + return new String(result); + } + + /** Decodes the hex-encoded bytes and returns their value a byte string. */ + public static ByteString decodeHex(String hex) { + if (hex == null) throw new IllegalArgumentException("hex == null"); + if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex); + + byte[] result = new byte[hex.length() / 2]; + for (int i = 0; i < result.length; i++) { + int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4; + int d2 = decodeHexDigit(hex.charAt(i * 2 + 1)); + result[i] = (byte) (d1 + d2); + } + return of(result); + } + + private static int decodeHexDigit(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + throw new IllegalArgumentException("Unexpected hex digit: " + c); + } + + /** + * Reads {@code count} bytes from {@code in} and returns the result. + * + * @throws java.io.EOFException if {@code in} has fewer than {@code count} + * bytes to read. + */ + public static ByteString read(InputStream in, int byteCount) throws IOException { + if (in == null) throw new IllegalArgumentException("in == null"); + if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount); + + byte[] result = new byte[byteCount]; + for (int offset = 0, read; offset < byteCount; offset += read) { + read = in.read(result, offset, byteCount - offset); + if (read == -1) throw new EOFException(); + } + return new ByteString(result); + } + + /** + * Returns a byte string equal to this byte string, but with the bytes 'A' + * through 'Z' replaced with the corresponding byte in 'a' through 'z'. + * Returns this byte string if it contains no bytes in 'A' through 'Z'. + */ + public ByteString toAsciiLowercase() { + // Search for an uppercase character. If we don't find one, return this. + for (int i = 0; i < data.length; i++) { + byte c = data[i]; + if (c < 'A' || c > 'Z') continue; + + // If we reach this point, this string is not not lowercase. Create and + // return a new byte string. + byte[] lowercase = data.clone(); + lowercase[i++] = (byte) (c - ('A' - 'a')); + for (; i < lowercase.length; i++) { + c = lowercase[i]; + if (c < 'A' || c > 'Z') continue; + lowercase[i] = (byte) (c - ('A' - 'a')); + } + return new ByteString(lowercase); + } + return this; + } + + /** + * Returns a byte string equal to this byte string, but with the bytes 'a' + * through 'z' replaced with the corresponding byte in 'A' through 'Z'. + * Returns this byte string if it contains no bytes in 'a' through 'z'. + */ + public ByteString toAsciiUppercase() { + // Search for an lowercase character. If we don't find one, return this. + for (int i = 0; i < data.length; i++) { + byte c = data[i]; + if (c < 'a' || c > 'z') continue; + + // If we reach this point, this string is not not uppercase. Create and + // return a new byte string. + byte[] lowercase = data.clone(); + lowercase[i++] = (byte) (c - ('a' - 'A')); + for (; i < lowercase.length; i++) { + c = lowercase[i]; + if (c < 'a' || c > 'z') continue; + lowercase[i] = (byte) (c - ('a' - 'A')); + } + return new ByteString(lowercase); + } + return this; + } + + /** Returns the byte at {@code pos}. */ + public byte getByte(int pos) { + return data[pos]; + } + + /** + * Returns the number of bytes in this ByteString. + */ + public int size() { + return data.length; + } + + /** + * Returns a byte array containing a copy of the bytes in this {@code ByteString}. + */ + public byte[] toByteArray() { + return data.clone(); + } + + /** Writes the contents of this byte string to {@code out}. */ + public void write(OutputStream out) throws IOException { + if (out == null) throw new IllegalArgumentException("out == null"); + out.write(data); + } + + @Override public boolean equals(Object o) { + return o == this || o instanceof ByteString && Arrays.equals(((ByteString) o).data, data); + } + + @Override public int hashCode() { + int result = hashCode; + return result != 0 ? result : (hashCode = Arrays.hashCode(data)); + } + + @Override public String toString() { + if (data.length == 0) { + return "ByteString[size=0]"; + } + + if (data.length <= 16) { + return String.format("ByteString[size=%s data=%s]", data.length, hex()); + } + + try { + return String.format("ByteString[size=%s md5=%s]", data.length, + ByteString.of(MessageDigest.getInstance("MD5").digest(data)).hex()); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(); + } + } + + private void readObject(ObjectInputStream in) throws IOException { + int dataLength = in.readInt(); + ByteString byteString = ByteString.read(in, dataLength); + try { + Field field = ByteString.class.getDeclaredField("data"); + field.setAccessible(true); + field.set(this, byteString.data); + } catch (NoSuchFieldException e) { + throw new AssertionError(); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(data.length); + out.write(data); + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/ErrorCode.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ErrorCode.java similarity index 96% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/ErrorCode.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/ErrorCode.java index 9a83aaae5..11ed9f255 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/ErrorCode.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/ErrorCode.java @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; // http://tools.ietf.org/html/draft-ietf-httpbis-http2-13#section-7 -public enum ErrorCode { +enum ErrorCode { /** Not an error! For SPDY stream resets, prefer null over NO_ERROR. */ NO_ERROR(0, -1, 0), diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/FrameReader.java similarity index 97% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/FrameReader.java index 674d80c52..1cbf2bfdc 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/FrameReader.java @@ -14,17 +14,16 @@ * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; import java.util.List; /** * Reads transport frames for SPDY/3 or HTTP/2. */ -public interface FrameReader { +interface FrameReader { // void readConnectionPreface() throws IOException; // boolean nextFrame(Handler handler) throws IOException; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/FrameWriter.java similarity index 97% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/FrameWriter.java index 5bbf060c2..b82a794de 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/FrameWriter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/FrameWriter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; import com.koushikdutta.async.ByteBufferList; @@ -23,7 +23,7 @@ import java.util.List; /** Writes transport frames for SPDY/3 or HTTP/2. */ -public interface FrameWriter extends Closeable { +interface FrameWriter extends Closeable { /** HTTP/2 only. */ void connectionPreface() throws IOException; void ackSettings() throws IOException; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Header.java similarity index 92% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/Header.java index bf9aaeb53..610c816b3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Header.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Header.java @@ -1,10 +1,8 @@ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; -import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; - /** HTTP header: the name is an ASCII string, but the value can be UTF-8. */ -public final class Header { +final class Header { // Special header names defined in the SPDY and HTTP/2 specs. public static final ByteString RESPONSE_STATUS = ByteString.encodeUtf8(":status"); public static final ByteString TARGET_METHOD = ByteString.encodeUtf8(":method"); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/HeaderReader.java similarity index 92% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/HeaderReader.java index 0df4ec83a..f8dbbf435 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeaderReader.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/HeaderReader.java @@ -1,7 +1,6 @@ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; import java.io.IOException; import java.nio.ByteBuffer; @@ -14,7 +13,7 @@ /** * Created by koush on 7/27/14. */ -public class HeaderReader { +class HeaderReader { Inflater inflater; public HeaderReader() { inflater = new Inflater() { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeadersMode.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/HeadersMode.java similarity index 95% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeadersMode.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/HeadersMode.java index 7ec54b58a..ebb72330d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HeadersMode.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/HeadersMode.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; public enum HeadersMode { SPDY_SYN_STREAM, diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/HpackDraft08.java similarity index 99% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/HpackDraft08.java index 8ef4aa593..a77fce4bf 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/HpackDraft08.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/HpackDraft08.java @@ -13,11 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okhttp.internal.BitArray; -import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/Http20Draft13.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Http20Draft13.java new file mode 100644 index 000000000..d1ec33948 --- /dev/null +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Http20Draft13.java @@ -0,0 +1,764 @@ +/* + * 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.koushikdutta.async.http.spdy; + +import com.koushikdutta.async.BufferedDataSink; +import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.DataEmitter; +import com.koushikdutta.async.DataEmitterReader; +import com.koushikdutta.async.callback.DataCallback; +import com.koushikdutta.async.http.Protocol; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.List; +import java.util.logging.Logger; + +import static com.koushikdutta.async.http.spdy.Http20Draft13.FrameLogger.formatHeader; +import static java.lang.String.format; +import static java.util.logging.Level.FINE; + +/** + * Read and write HTTP/2 v13 frames. + *

http://tools.ietf.org/html/draft-ietf-httpbis-http2-13 + */ + +final class Http20Draft13 implements Variant { + private static final Logger logger = Logger.getLogger(Http20Draft13.class.getName()); + + @Override + public Protocol getProtocol() { + return Protocol.HTTP_2; + } + + private static final ByteString CONNECTION_PREFACE + = ByteString.encodeUtf8("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"); + + static final int MAX_FRAME_SIZE = 0x3fff; // 16383 + + static final byte TYPE_DATA = 0x0; + static final byte TYPE_HEADERS = 0x1; + static final byte TYPE_PRIORITY = 0x2; + static final byte TYPE_RST_STREAM = 0x3; + static final byte TYPE_SETTINGS = 0x4; + static final byte TYPE_PUSH_PROMISE = 0x5; + static final byte TYPE_PING = 0x6; + static final byte TYPE_GOAWAY = 0x7; + static final byte TYPE_WINDOW_UPDATE = 0x8; + static final byte TYPE_CONTINUATION = 0x9; + + static final byte FLAG_NONE = 0x0; + static final byte FLAG_ACK = 0x1; // Used for settings and ping. + static final byte FLAG_END_STREAM = 0x1; // Used for headers and data. + static final byte FLAG_END_SEGMENT = 0x2; + static final byte FLAG_END_HEADERS = 0x4; // Used for headers and continuation. + static final byte FLAG_END_PUSH_PROMISE = 0x4; + static final byte FLAG_PADDED = 0x8; // Used for headers and data. + static final byte FLAG_PRIORITY = 0x20; // Used for headers. + static final byte FLAG_COMPRESSED = 0x20; // Used for data. + + /** + * Creates a frame reader with max header table size of 4096 and data frame + * compression disabled. + */ + @Override + public FrameReader newReader(DataEmitter source, FrameReader.Handler handler, boolean client) { + return new Reader(source, handler, 4096, client); + } + + @Override + public FrameWriter newWriter(BufferedDataSink sink, boolean client) { + return new Writer(sink, client); + } + + @Override + public int maxFrameSize() { + return MAX_FRAME_SIZE; + } + + static final class Reader implements FrameReader { + private final DataEmitter emitter; + private final boolean client; + private final Handler handler; + private final DataEmitterReader reader; + + // Visible for testing. + final HpackDraft08.Reader hpackReader; + + Reader(DataEmitter emitter, Handler handler, int headerTableSize, boolean client) { + this.emitter = emitter; + this.client = client; + this.hpackReader = new HpackDraft08.Reader(headerTableSize); + this.handler = handler; + reader = new DataEmitterReader(); + + parseFrameHeader(); + } + + private void parseFrameHeader() { + emitter.setDataCallback(reader); + reader.read(8, onFrame); + } + + int w1; + int w2; + byte flags; + byte type; + short length; + int streamId; + private final DataCallback onFrame = new DataCallback() { + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + bb.order(ByteOrder.BIG_ENDIAN); + w1 = bb.getInt(); + w2 = bb.getInt(); + + // boolean r = (w1 & 0xc0000000) != 0; // Reserved: Ignore first 2 bits. + length = (short) ((w1 & 0x3fff0000) >> 16); // 14-bit unsigned == MAX_FRAME_SIZE + type = (byte) ((w1 & 0xff00) >> 8); + flags = (byte) (w1 & 0xff); + // boolean r = (w2 & 0x80000000) != 0; // Reserved: Ignore first bit. + streamId = (w2 & 0x7fffffff); // 31-bit opaque identifier. + if (logger.isLoggable(FINE)) + logger.fine(formatHeader(true, streamId, length, type, flags)); + + reader.read(length, onFullFrame); + } + }; + + private final DataCallback onFullFrame = new DataCallback() { + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + try { + switch (type) { + case TYPE_DATA: + readData(bb, length, flags, streamId); + break; + + case TYPE_HEADERS: + readHeaders(bb, length, flags, streamId); + break; + + case TYPE_PRIORITY: + readPriority(bb, length, flags, streamId); + break; + + case TYPE_RST_STREAM: + readRstStream(bb, length, flags, streamId); + break; + + case TYPE_SETTINGS: + readSettings(bb, length, flags, streamId); + break; + + case TYPE_PUSH_PROMISE: + readPushPromise(bb, length, flags, streamId); + break; + + case TYPE_PING: + readPing(bb, length, flags, streamId); + break; + + case TYPE_GOAWAY: + readGoAway(bb, length, flags, streamId); + break; + + case TYPE_WINDOW_UPDATE: + readWindowUpdate(bb, length, flags, streamId); + break; + + case TYPE_CONTINUATION: + readContinuation(bb, length, flags, streamId); + break; + + default: + // Implementations MUST discard frames that have unknown or unsupported types. + bb.recycle(); + } + parseFrameHeader(); + } + catch (IOException e) { + handler.error(e); + } + } + }; + + /* + @Override + public void readConnectionPreface() throws IOException { + if (client) return; // Nothing to read; servers doesn't send a connection preface! + ByteString connectionPreface = source.readByteString(CONNECTION_PREFACE.size()); + if (logger.isLoggable(FINE)) + logger.fine(format("<< CONNECTION %s", connectionPreface.hex())); + if (!CONNECTION_PREFACE.equals(connectionPreface)) { + throw ioException("Expected a connection header but was %s", connectionPreface.utf8()); + } + } + */ + + byte pendingHeaderType; + private void readHeaders(ByteBufferList source, short length, byte flags, int streamId) + throws IOException { + if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0"); + + + short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.get() & 0xff) : 0; + + if ((flags & FLAG_PRIORITY) != 0) { + readPriority(source, streamId); + length -= 5; // account for above read. + } + + length = lengthWithoutPadding(length, flags, padding); + + pendingHeaderType = type; + readHeaderBlock(source, length, padding, flags, streamId); + +// handler.headers(false, endStream, streamId, -1, headerBlock, HeadersMode.HTTP_20_HEADERS); + } + + private void readContinuation(ByteBufferList source, short length, byte flags, int streamId) + throws IOException { + if (streamId != continuingStreamId) + throw new IOException("continuation stream id mismatch"); + readHeaderBlock(source, length, (short)0, flags, streamId); + } + + int continuingStreamId; + private void readHeaderBlock(ByteBufferList source, short length, short padding, byte flags, int streamId) + throws IOException { + source.skip(padding); + hpackReader.refill(source); + hpackReader.readHeaders(); + hpackReader.emitReferenceSet(); + // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20. + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3 + if ((flags & FLAG_END_HEADERS) != 0) { + if (pendingHeaderType == TYPE_HEADERS) { + boolean endStream = (flags & FLAG_END_STREAM) != 0; + handler.headers(false, endStream, streamId, -1, hpackReader.getAndReset(), HeadersMode.HTTP_20_HEADERS); + } + else if (pendingHeaderType == TYPE_PUSH_PROMISE) { + handler.pushPromise(streamId, promisedStreamId, hpackReader.getAndReset()); + } + else { + throw new AssertionError("unknown header type"); + } + } + else { + continuingStreamId = streamId; + } + } + + private void readData(ByteBufferList source, short length, byte flags, int streamId) + throws IOException { + // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED + boolean inFinished = (flags & FLAG_END_STREAM) != 0; + boolean gzipped = (flags & FLAG_COMPRESSED) != 0; + if (gzipped) { + throw ioException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA"); + } + + short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.get() & 0xff) : 0; + length = lengthWithoutPadding(length, flags, padding); + + handler.data(inFinished, streamId, source); + source.skip(padding); + } + + private void readPriority(ByteBufferList source, short length, byte flags, int streamId) + throws IOException { + if (length != 5) throw ioException("TYPE_PRIORITY length: %d != 5", length); + if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0"); + readPriority(source, streamId); + } + + private void readPriority(ByteBufferList source, int streamId) throws IOException { + int w1 = source.getInt(); + boolean exclusive = (w1 & 0x80000000) != 0; + int streamDependency = (w1 & 0x7fffffff); + int weight = (source.get() & 0xff) + 1; + handler.priority(streamId, streamDependency, weight, exclusive); + } + + private void readRstStream(ByteBufferList source, short length, byte flags, 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 = source.getInt(); + 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(ByteBufferList source, short length, byte flags, int streamId) + throws IOException { + if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0"); + if ((flags & FLAG_ACK) != 0) { + if (length != 0) throw ioException("FRAME_SIZE_ERROR ack frame should be empty!"); + handler.ackSettings(); + return; + } + + if (length % 6 != 0) throw ioException("TYPE_SETTINGS length %% 6 != 0: %s", length); + Settings settings = new Settings(); + for (int i = 0; i < length; i += 6) { + short id = source.getShort(); + int value = source.getInt(); + + switch (id) { + case 1: // SETTINGS_HEADER_TABLE_SIZE + break; + case 2: // SETTINGS_ENABLE_PUSH + if (value != 0 && value != 1) { + throw ioException("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1"); + } + break; + case 3: // SETTINGS_MAX_CONCURRENT_STREAMS + id = 4; // Renumbered in draft 10. + break; + case 4: // SETTINGS_INITIAL_WINDOW_SIZE + id = 7; // Renumbered in draft 10. + if (value < 0) { + throw ioException("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1"); + } + break; + case 5: // SETTINGS_COMPRESS_DATA + break; + default: + throw ioException("PROTOCOL_ERROR invalid settings id: %s", id); + } + settings.set(id, 0, value); + } + handler.settings(false, settings); + if (settings.getHeaderTableSize() >= 0) { + hpackReader.maxHeaderTableByteCountSetting(settings.getHeaderTableSize()); + } + } + + int promisedStreamId; + private void readPushPromise(ByteBufferList source, short length, byte flags, int streamId) + throws IOException { + if (streamId == 0) { + throw ioException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0"); + } + short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.get() & 0xff) : 0; + promisedStreamId = source.getInt() & 0x7fffffff; + length -= 4; // account for above read. + length = lengthWithoutPadding(length, flags, padding); + pendingHeaderType = TYPE_PUSH_PROMISE; + readHeaderBlock(source, length, padding, flags, streamId); + } + + private void readPing(ByteBufferList source, short length, byte flags, 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 = source.getInt(); + int payload2 = source.getInt(); + boolean ack = (flags & FLAG_ACK) != 0; + handler.ping(ack, payload1, payload2); + } + + private void readGoAway(ByteBufferList source, short length, byte flags, int streamId) + throws IOException { + if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length); + if (streamId != 0) throw ioException("TYPE_GOAWAY streamId != 0"); + int lastStreamId = source.getInt(); + int errorCodeInt = source.getInt(); + int opaqueDataLength = length - 8; + ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt); + if (errorCode == null) { + throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt); + } + ByteString debugData = ByteString.EMPTY; + if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection. + debugData = ByteString.of(source.getBytes(opaqueDataLength)); + } + handler.goAway(lastStreamId, errorCode, debugData); + } + + private void readWindowUpdate(ByteBufferList source, short length, byte flags, int streamId) + throws IOException { + if (length != 4) throw ioException("TYPE_WINDOW_UPDATE length !=4: %s", length); + long increment = (source.getInt() & 0x7fffffffL); + if (increment == 0) throw ioException("windowSizeIncrement was 0", increment); + handler.windowUpdate(streamId, increment); + } + } + + static final class Writer implements FrameWriter { + private final BufferedDataSink sink; + private final boolean client; + private final HpackDraft08.Writer hpackWriter; + private boolean closed; + private final ByteBufferList frameHeader = new ByteBufferList(); + + Writer(BufferedDataSink sink, boolean client) { + this.sink = sink; + this.client = client; + this.hpackWriter = new HpackDraft08.Writer(); + } + + @Override + public synchronized void ackSettings() throws IOException { + if (closed) throw new IOException("closed"); + int length = 0; + byte type = TYPE_SETTINGS; + byte flags = FLAG_ACK; + int streamId = 0; + frameHeader(streamId, length, type, flags); + } + + @Override + public synchronized void connectionPreface() throws IOException { + if (closed) throw new IOException("closed"); + if (!client) return; // Nothing to write; servers don't send connection headers! + if (logger.isLoggable(FINE)) { + logger.fine(format(">> CONNECTION %s", CONNECTION_PREFACE.hex())); + } + sink.write(new ByteBufferList(CONNECTION_PREFACE.toByteArray())); + } + + @Override + public synchronized void synStream(boolean outFinished, boolean inFinished, + int streamId, int associatedStreamId, List

headerBlock) + throws IOException { + if (inFinished) throw new UnsupportedOperationException(); + if (closed) throw new IOException("closed"); + headers(outFinished, streamId, headerBlock); + } + + @Override + public synchronized void synReply(boolean outFinished, int streamId, + List
headerBlock) throws IOException { + if (closed) throw new IOException("closed"); + headers(outFinished, streamId, headerBlock); + } + + @Override + public synchronized void headers(int streamId, List
headerBlock) + throws IOException { + if (closed) throw new IOException("closed"); + headers(false, streamId, headerBlock); + } + + @Override + public synchronized void pushPromise(int streamId, int promisedStreamId, + List
requestHeaders) throws IOException { + if (closed) throw new IOException("closed"); + ByteBufferList hpackBuffer = hpackWriter.writeHeaders(requestHeaders); + + long byteCount = hpackBuffer.remaining(); + int length = (int) Math.min(MAX_FRAME_SIZE - 4, byteCount); + byte type = TYPE_PUSH_PROMISE; + byte flags = byteCount == length ? FLAG_END_HEADERS : 0; + frameHeader(streamId, length + 4, type, flags); + ByteBuffer sink = ByteBufferList.obtain(8192).order(ByteOrder.BIG_ENDIAN); + sink.putInt(promisedStreamId & 0x7fffffff); + sink.flip(); + frameHeader.add(sink); + hpackBuffer.get(frameHeader, length); + this.sink.write(frameHeader); + + if (byteCount > length) writeContinuationFrames(hpackBuffer, streamId); + } + + void headers(boolean outFinished, int streamId, List
headerBlock) throws IOException { + if (closed) throw new IOException("closed"); + ByteBufferList hpackBuffer = hpackWriter.writeHeaders(headerBlock); + + long byteCount = hpackBuffer.remaining(); + int length = (int) Math.min(MAX_FRAME_SIZE, byteCount); + byte type = TYPE_HEADERS; + byte flags = byteCount == length ? FLAG_END_HEADERS : 0; + if (outFinished) flags |= FLAG_END_STREAM; + frameHeader(streamId, length, type, flags); + hpackBuffer.get(frameHeader, length); + this.sink.write(frameHeader); + + if (byteCount > length) writeContinuationFrames(hpackBuffer, streamId); + } + + private void writeContinuationFrames(ByteBufferList hpackBuffer, int streamId) throws IOException { + while (hpackBuffer.hasRemaining()) { + int length = (int) Math.min(MAX_FRAME_SIZE, hpackBuffer.remaining()); + int newRemaining = hpackBuffer.remaining() - length; + frameHeader(streamId, length, TYPE_CONTINUATION, newRemaining == 0 ? FLAG_END_HEADERS : 0); + hpackBuffer.get(frameHeader, length); + sink.write(frameHeader); + } + } + + @Override + public synchronized void rstStream(int streamId, ErrorCode errorCode) + throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException(); + + int length = 4; + byte type = TYPE_RST_STREAM; + byte flags = FLAG_NONE; + frameHeader(streamId, length, type, flags); + ByteBuffer sink = ByteBufferList.obtain(8192).order(ByteOrder.BIG_ENDIAN); + sink.putInt(errorCode.httpCode); + sink.flip(); + this.sink.write(frameHeader.add(sink)); + } + + @Override + public synchronized void data(boolean outFinished, int streamId, ByteBufferList source) + throws IOException { + if (closed) throw new IOException("closed"); + byte flags = FLAG_NONE; + if (outFinished) flags |= FLAG_END_STREAM; + dataFrame(streamId, flags, source); + } + + void dataFrame(int streamId, byte flags, ByteBufferList buffer) throws IOException { + byte type = TYPE_DATA; + frameHeader(streamId, buffer.remaining(), type, flags); + sink.write(buffer); + } + + @Override + public synchronized void settings(Settings settings) throws IOException { + if (closed) throw new IOException("closed"); + int length = settings.size() * 6; + byte type = TYPE_SETTINGS; + byte flags = FLAG_NONE; + int streamId = 0; + frameHeader(streamId, length, type, flags); + ByteBuffer sink = ByteBufferList.obtain(8192).order(ByteOrder.BIG_ENDIAN); + for (int i = 0; i < Settings.COUNT; i++) { + if (!settings.isSet(i)) continue; + int id = i; + if (id == 4) id = 3; // SETTINGS_MAX_CONCURRENT_STREAMS renumbered. + else if (id == 7) id = 4; // SETTINGS_INITIAL_WINDOW_SIZE renumbered. + sink.putShort((short) id); + sink.putInt(settings.get(i)); + } + sink.flip(); + this.sink.write(frameHeader.add(sink)); + } + + @Override + public synchronized void ping(boolean ack, int payload1, int payload2) + throws IOException { + if (closed) throw new IOException("closed"); + int length = 8; + byte type = TYPE_PING; + byte flags = ack ? FLAG_ACK : FLAG_NONE; + int streamId = 0; + frameHeader(streamId, length, type, flags); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(payload1); + sink.putInt(payload2); + sink.flip(); + this.sink.write(frameHeader.add(sink)); + } + + @Override + public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode, + byte[] debugData) throws IOException { + if (closed) throw new IOException("closed"); + if (errorCode.httpCode == -1) throw illegalArgument("errorCode.httpCode == -1"); + int length = 8 + debugData.length; + byte type = TYPE_GOAWAY; + byte flags = FLAG_NONE; + int streamId = 0; + frameHeader(streamId, length, type, flags); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt(lastGoodStreamId); + sink.putInt(errorCode.httpCode); + sink.put(debugData); + sink.flip(); + this.sink.write(frameHeader.add(sink)); + } + + @Override + public synchronized void windowUpdate(int streamId, long windowSizeIncrement) + throws IOException { + if (closed) throw new IOException("closed"); + if (windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL) { + throw illegalArgument("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: %s", + windowSizeIncrement); + } + int length = 4; + byte type = TYPE_WINDOW_UPDATE; + byte flags = FLAG_NONE; + frameHeader(streamId, length, type, flags); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt((int) windowSizeIncrement); + sink.flip(); + this.sink.write(frameHeader.add(sink)); + } + + @Override + public synchronized void close() throws IOException { + closed = true; + } + + void frameHeader(int streamId, int length, byte type, byte flags) throws IOException { + if (logger.isLoggable(FINE)) + logger.fine(formatHeader(false, streamId, length, type, flags)); + if (length > MAX_FRAME_SIZE) { + throw illegalArgument("FRAME_SIZE_ERROR length > %d: %d", MAX_FRAME_SIZE, length); + } + if ((streamId & 0x80000000) != 0) + throw illegalArgument("reserved bit set: %s", streamId); + ByteBuffer sink = ByteBufferList.obtain(256).order(ByteOrder.BIG_ENDIAN); + sink.putInt((length & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff)); + sink.putInt(streamId & 0x7fffffff); + sink.flip(); + this.sink.write(frameHeader.add(sink)); + } + } + + private static IllegalArgumentException illegalArgument(String message, Object... args) { + throw new IllegalArgumentException(format(message, args)); + } + + private static IOException ioException(String message, Object... args) throws IOException { + throw new IOException(format(message, args)); + } + + private static short lengthWithoutPadding(short length, byte flags, short padding) + throws IOException { + if ((flags & FLAG_PADDED) != 0) length--; // Account for reading the padding length. + if (padding > length) { + throw ioException("PROTOCOL_ERROR padding %s > remaining length %s", padding, length); + } + return (short) (length - padding); + } + + /** + * Logs a human-readable representation of HTTP/2 frame headers. + *

+ *

The format is: + *

+ *

+     *   direction streamID length type flags
+     * 
+ * Where direction is {@code <<} for inbound and {@code >>} for outbound. + *

+ *

For example, the following would indicate a HEAD request sent from + * the client. + *

+     * {@code
+     *   << 0x0000000f    12 HEADERS       END_HEADERS|END_STREAM
+     * }
+     * 
+ */ + static final class FrameLogger { + + static String formatHeader(boolean inbound, int streamId, int length, byte type, byte flags) { + String formattedType = type < TYPES.length ? TYPES[type] : format("0x%02x", type); + String formattedFlags = formatFlags(type, flags); + return format("%s 0x%08x %5d %-13s %s", inbound ? "<<" : ">>", streamId, length, + formattedType, formattedFlags); + } + + /** + * Looks up valid string representing flags from the table. Invalid + * combinations are represented in binary. + */ + // Visible for testing. + static String formatFlags(byte type, byte flags) { + if (flags == 0) return ""; + switch (type) { // Special case types that have 0 or 1 flag. + case TYPE_SETTINGS: + case TYPE_PING: + return flags == FLAG_ACK ? "ACK" : BINARY[flags]; + case TYPE_PRIORITY: + case TYPE_RST_STREAM: + case TYPE_GOAWAY: + case TYPE_WINDOW_UPDATE: + return BINARY[flags]; + } + String result = flags < FLAGS.length ? FLAGS[flags] : BINARY[flags]; + // Special case types that have overlap flag values. + if (type == TYPE_PUSH_PROMISE && (flags & FLAG_END_PUSH_PROMISE) != 0) { + return result.replace("HEADERS", "PUSH_PROMISE"); // TODO: Avoid allocation. + } else if (type == TYPE_DATA && (flags & FLAG_COMPRESSED) != 0) { + return result.replace("PRIORITY", "COMPRESSED"); // TODO: Avoid allocation. + } + return result; + } + + /** + * Lookup table for valid frame types. + */ + private static final String[] TYPES = new String[]{ + "DATA", + "HEADERS", + "PRIORITY", + "RST_STREAM", + "SETTINGS", + "PUSH_PROMISE", + "PING", + "GOAWAY", + "WINDOW_UPDATE", + "CONTINUATION" + }; + + /** + * Lookup table for valid flags for DATA, HEADERS, CONTINUATION. Invalid + * combinations are represented in binary. + */ + private static final String[] FLAGS = new String[0x40]; // Highest bit flag is 0x20. + private static final String[] BINARY = new String[256]; + + static { + for (int i = 0; i < BINARY.length; i++) { + BINARY[i] = format("%8s", Integer.toBinaryString(i)).replace(' ', '0'); + } + + FLAGS[FLAG_NONE] = ""; + FLAGS[FLAG_END_STREAM] = "END_STREAM"; + FLAGS[FLAG_END_SEGMENT] = "END_SEGMENT"; + FLAGS[FLAG_END_STREAM | FLAG_END_SEGMENT] = "END_STREAM|END_SEGMENT"; + int[] prefixFlags = + new int[]{FLAG_END_STREAM, FLAG_END_SEGMENT, FLAG_END_SEGMENT | FLAG_END_STREAM}; + + FLAGS[FLAG_PADDED] = "PADDED"; + for (int prefixFlag : prefixFlags) { + FLAGS[prefixFlag | FLAG_PADDED] = FLAGS[prefixFlag] + "|PADDED"; + } + + FLAGS[FLAG_END_HEADERS] = "END_HEADERS"; // Same as END_PUSH_PROMISE. + FLAGS[FLAG_PRIORITY] = "PRIORITY"; // Same as FLAG_COMPRESSED. + FLAGS[FLAG_END_HEADERS | FLAG_PRIORITY] = "END_HEADERS|PRIORITY"; // Only valid on HEADERS. + int[] frameFlags = + new int[]{FLAG_END_HEADERS, FLAG_PRIORITY, FLAG_END_HEADERS | FLAG_PRIORITY}; + + for (int frameFlag : frameFlags) { + for (int prefixFlag : prefixFlags) { + FLAGS[prefixFlag | frameFlag] = FLAGS[prefixFlag] + '|' + FLAGS[frameFlag]; + FLAGS[prefixFlag | frameFlag | FLAG_PADDED] = + FLAGS[prefixFlag] + '|' + FLAGS[frameFlag] + "|PADDED"; + } + } + + for (int i = 0; i < FLAGS.length; i++) { // Fill in holes with binary representation. + if (FLAGS[i] == null) FLAGS[i] = BINARY[i]; + } + } + } +} diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Huffman.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Huffman.java similarity index 99% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Huffman.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/Huffman.java index dfa7153ef..2472a03b3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Huffman.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Huffman.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Ping.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Ping.java similarity index 95% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Ping.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/Ping.java index 0a82b4308..9f5ae8f5d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Ping.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Ping.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -21,7 +21,7 @@ /** * A locally-originated ping. */ -public final class Ping { +final class Ping { private final CountDownLatch latch = new CountDownLatch(1); private long sent = -1; private long received = -1; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Settings.java similarity index 98% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/Settings.java index 1b96f64a5..726816f11 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Settings.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Settings.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; import java.util.Arrays; @@ -21,7 +21,7 @@ * Settings describe characteristics of the sending peer, which are used by the receiving peer. * Settings are {@link com.koushikdutta.async.http.spdy.okhttp.internal.spdy.SpdyConnection connection} scoped. */ -public final class Settings { +final class Settings { /** * From the SPDY/3 and HTTP/2 specs, the default initial window size for all * streams is 64 KiB. (Chrome 25 uses 10 MiB). diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Spdy3.java similarity index 99% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/Spdy3.java index 52155ca04..35f4e6e3b 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Spdy3.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Spdy3.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.ByteBufferList; @@ -21,7 +21,6 @@ import com.koushikdutta.async.DataEmitterReader; import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.http.spdy.okhttp.internal.ByteString; import com.koushikdutta.async.util.Charsets; import java.io.IOException; @@ -37,7 +36,7 @@ * Read and write spdy/3.1 frames. * http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1 */ -public final class Spdy3 implements Variant { +final class Spdy3 implements Variant { @Override public Protocol getProtocol() { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index b97f261be..1ac43d5d8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -16,7 +16,6 @@ import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; import com.koushikdutta.async.util.Charsets; import java.lang.reflect.Field; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java index fb29b6a72..a2e3029e2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java @@ -18,7 +18,6 @@ import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.http.spdy.okhttp.internal.Util; import java.util.List; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Util.java similarity index 94% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/Util.java index 6c8c0e4f1..288ba2640 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Util.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal; +package com.koushikdutta.async.http.spdy; import java.util.ArrayList; import java.util.Arrays; @@ -22,7 +22,7 @@ import java.util.List; /** Junk drawer of utility methods. */ -public final class Util { +final class Util { public static void checkOffsetAndCount(long arrayLength, long offset, long count) { if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) { throw new ArrayIndexOutOfBoundsException(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Variant.java similarity index 93% rename from AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java rename to AndroidAsync/src/com/koushikdutta/async/http/spdy/Variant.java index 00b12ffaf..d54a48470 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/okhttp/internal/spdy/Variant.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Variant.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.koushikdutta.async.http.spdy.okhttp.internal.spdy; +package com.koushikdutta.async.http.spdy; import com.koushikdutta.async.BufferedDataSink; @@ -21,7 +21,7 @@ import com.koushikdutta.async.http.Protocol; /** A version and dialect of the framed socket protocol. */ -public interface Variant { +interface Variant { /** The protocol as selected using NPN or ALPN. */ Protocol getProtocol(); diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java index a50d910f7..cd5014883 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java @@ -18,12 +18,12 @@ import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.ErrorCode; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.FrameReader; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Header; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.HeadersMode; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Settings; -import com.koushikdutta.async.http.spdy.okhttp.internal.spdy.Spdy3; +import com.koushikdutta.async.http.spdy.ErrorCode; +import com.koushikdutta.async.http.spdy.FrameReader; +import com.koushikdutta.async.http.spdy.Header; +import com.koushikdutta.async.http.spdy.HeadersMode; +import com.koushikdutta.async.http.spdy.Settings; +import com.koushikdutta.async.http.spdy.Spdy3; import com.koushikdutta.async.http.spdy.okio.BufferedSource; import com.koushikdutta.async.http.spdy.okio.ByteString; import com.koushikdutta.async.http.spdy.okio.Okio; diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java b/AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java index e53908f72..597436b6a 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java @@ -1,6 +1,6 @@ package com.koushikdutta.async.test; -import com.koushikdutta.async.http.spdy.okhttp.internal.Util; +import com.koushikdutta.async.http.spdy.Util; import java.security.Principal; import java.security.cert.Certificate; From 1280d7b44e14dbac19f773136742224bcab91302 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 20:07:37 -0700 Subject: [PATCH 060/399] spdy posting works. --- .../async/http/AsyncHttpClient.java | 24 +- .../async/http/AsyncHttpClientMiddleware.java | 29 ++- .../async/http/AsyncHttpResponseImpl.java | 3 +- .../async/http/AsyncSocketMiddleware.java | 2 +- .../com/koushikdutta/async/http/Headers.java | 19 +- .../async/http/HttpTransportMiddleware.java | 14 +- .../async/http/SimpleMiddleware.java | 14 +- .../http/body/MultipartFormDataBody.java | 6 +- .../async/http/body/UrlEncodedFormBody.java | 1 + .../http/cache/ResponseCacheMiddleware.java | 4 +- .../http/filter/ChunkedOutputFilter.java | 3 +- .../async/http/server/BoundaryEmitter.java | 1 - .../async/http/spdy/AsyncSpdyConnection.java | 30 ++- .../koushikdutta/async/http/spdy/Spdy3.java | 2 +- .../async/http/spdy/SpdyMiddleware.java | 235 ++++++++++-------- 15 files changed, 236 insertions(+), 151 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index ee4f8054c..49c7de506 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -17,7 +17,6 @@ import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.future.SimpleFuture; -import com.koushikdutta.async.http.AsyncHttpClientMiddleware.OnRequestCompleteData; import com.koushikdutta.async.http.callback.HttpConnectCallback; import com.koushikdutta.async.http.callback.RequestCallback; import com.koushikdutta.async.parser.AsyncParser; @@ -204,7 +203,7 @@ private void executeAffinity(final AsyncHttpRequest request, final int redirectC return; } final Uri uri = request.getUri(); - final OnRequestCompleteData data = new OnRequestCompleteData(); + final AsyncHttpClientMiddleware.OnResponseCompleteDataOnRequestSentData data = new AsyncHttpClientMiddleware.OnResponseCompleteDataOnRequestSentData(); request.executionTime = System.currentTimeMillis(); data.request = request; @@ -273,6 +272,12 @@ public void onConnectCompleted(Exception ex, AsyncSocket socket) { // set up the system default proxy and connect setupAndroidProxy(request); + // set the implicit content type + if (request.getBody() != null) { + if (request.getHeaders().get("Content-Type") == null) + request.getHeaders().set("Content-Type", request.getBody().getContentType()); + } + synchronized (mMiddleware) { for (AsyncHttpClientMiddleware middleware: mMiddleware) { Cancellable socketCancellable = middleware.getSocket(data); @@ -288,13 +293,18 @@ public void onConnectCompleted(Exception ex, AsyncSocket socket) { private void executeSocket(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback, - final OnRequestCompleteData data) { + final AsyncHttpClientMiddleware.OnResponseCompleteDataOnRequestSentData data) { // 4) wait for request to be sent fully // and // 6) wait for headers final AsyncHttpResponseImpl ret = new AsyncHttpResponseImpl(request) { @Override protected void onRequestCompleted(Exception ex) { + if (ex != null) { + reportConnectedCompleted(cancel, ex, null, request, callback); + return; + } + request.logv("request completed"); if (cancel.isCancelled()) return; @@ -303,6 +313,12 @@ protected void onRequestCompleted(Exception ex) { mServer.removeAllCallbacks(cancel.scheduled); cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request)); } + + synchronized (mMiddleware) { + for (AsyncHttpClientMiddleware middleware: mMiddleware) { + middleware.onRequestSent(data); + } + } } @Override @@ -402,7 +418,7 @@ protected void report(Exception ex) { data.exception = ex; synchronized (mMiddleware) { for (AsyncHttpClientMiddleware middleware: mMiddleware) { - middleware.onRequestComplete(data); + middleware.onResponseComplete(data); } } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java index edce25dda..48be20947 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClientMiddleware.java @@ -40,21 +40,24 @@ public static class GetSocketData extends OnRequestData { public String protocol; } - public static class ExchangeHeaderData extends GetSocketData { + public static class OnExchangeHeaderData extends GetSocketData { public AsyncSocket socket; public ResponseHead response; public CompletedCallback sendHeadersCallback; public CompletedCallback receiveHeadersCallback; } - public static class OnHeadersReceivedData extends ExchangeHeaderData { + public static class OnRequestSentData extends OnExchangeHeaderData { } - public static class OnBodyData extends OnHeadersReceivedData { + public static class OnHeadersReceivedDataOnRequestSentData extends OnRequestSentData { + } + + public static class OnBodyDataOnRequestSentData extends OnHeadersReceivedDataOnRequestSentData { public DataEmitter bodyEmitter; } - public static class OnRequestCompleteData extends OnBodyData { + public static class OnResponseCompleteDataOnRequestSentData extends OnBodyDataOnRequestSentData { public Exception exception; } @@ -77,23 +80,31 @@ public static class OnRequestCompleteData extends OnBodyData { * @param data * @return */ - public boolean exchangeHeaders(ExchangeHeaderData data); + public boolean exchangeHeaders(OnExchangeHeaderData data); + + /** + * Called once the headers and any optional request body has + * been sent + * @param data + */ + public void onRequestSent(OnRequestSentData data); /** * Called once the headers have been received via the socket * @param data */ - public void onHeadersReceived(OnHeadersReceivedData data); + public void onHeadersReceived(OnHeadersReceivedDataOnRequestSentData data); /** * Called before the body is decoded * @param data */ - public void onBodyDecoder(OnBodyData data); + public void onBodyDecoder(OnBodyDataOnRequestSentData data); /** - * Called once the request is complete + * Called once the request is complete and response has been received, + * or if an error occurred * @param data */ - public void onRequestComplete(OnRequestCompleteData data); + public void onResponseComplete(OnResponseCompleteDataOnRequestSentData data); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index 7938b935e..cd6ffffcd 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -193,8 +193,7 @@ public void write(ByteBufferList bb) { @Override public void end() { - if (mSink instanceof ChunkedOutputFilter) - mSink.end(); + throw new AssertionError("end called?"); } @Override diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java index 5c11684de..35ce3dd70 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java @@ -345,7 +345,7 @@ private void nextConnection(AsyncHttpRequest request) { } @Override - public void onRequestComplete(final OnRequestCompleteData data) { + public void onResponseComplete(final OnResponseCompleteDataOnRequestSentData data) { if (!data.state.get(getClass().getCanonicalName() + ".owned", false)) { return; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Headers.java b/AndroidAsync/src/com/koushikdutta/async/http/Headers.java index 8fb70253e..1b4cdc294 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/Headers.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/Headers.java @@ -29,20 +29,20 @@ public Multimap getMultiMap() { } public List getAll(String header) { - return map.get(header); + return map.get(header.toLowerCase()); } public String get(String header) { - return map.getString(header); + return map.getString(header.toLowerCase()); } public Headers set(String header, String value) { - map.put(header, value); + map.put(header.toLowerCase(), value); return this; } public Headers add(String header, String value) { - map.add(header, value); + map.add(header.toLowerCase(), value); return this; } @@ -66,21 +66,26 @@ public Headers addAll(String header, List values) { } public Headers addAll(Map> m) { - map.putAll(m); + for (String key: m.keySet()) { + for (String value: m.get(key)) { + add(key, value); + } + } return this; } public Headers addAll(Headers headers) { + // safe to addall since this is another Headers object map.putAll(headers.map); return this; } public List removeAll(String header) { - return map.remove(header); + return map.remove(header.toLowerCase()); } public String remove(String header) { - List r = removeAll(header); + List r = removeAll(header.toLowerCase()); if (r == null || r.size() == 0) return null; return r.get(0); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java index 837ca925f..6659193e1 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java @@ -14,7 +14,7 @@ */ public class HttpTransportMiddleware extends SimpleMiddleware { @Override - public boolean exchangeHeaders(final ExchangeHeaderData data) { + public boolean exchangeHeaders(final OnExchangeHeaderData data) { Protocol p = Protocol.get(data.protocol); if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1) return super.exchangeHeaders(data); @@ -23,8 +23,6 @@ public boolean exchangeHeaders(final ExchangeHeaderData data) { AsyncHttpRequestBody requestBody = data.request.getBody(); if (requestBody != null) { - if (request.getHeaders().get("Content-Type") == null) - request.getHeaders().set("Content-Type", requestBody.getContentType()); if (requestBody.length() >= 0) { request.getHeaders().set("Content-Length", String.valueOf(requestBody.length())); data.response.sink(data.socket); @@ -92,4 +90,14 @@ else if (!"\r".equals(s)) { liner.setLineCallback(headerCallback); return true; } + + @Override + public void onRequestSent(OnRequestSentData data) { + Protocol p = Protocol.get(data.protocol); + if (p != null && p != Protocol.HTTP_1_0 && p != Protocol.HTTP_1_1) + return; + + if (data.response.sink() instanceof ChunkedOutputFilter) + data.response.sink().end(); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java index 242fdabbc..8b2998ae8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java @@ -13,19 +13,23 @@ public Cancellable getSocket(GetSocketData data) { } @Override - public void onHeadersReceived(OnHeadersReceivedData data) { + public boolean exchangeHeaders(OnExchangeHeaderData data) { + return false; } @Override - public void onBodyDecoder(OnBodyData data) { + public void onRequestSent(OnRequestSentData data) { } @Override - public void onRequestComplete(OnRequestCompleteData data) { + public void onHeadersReceived(OnHeadersReceivedDataOnRequestSentData data) { } @Override - public boolean exchangeHeaders(ExchangeHeaderData data) { - return false; + public void onBodyDecoder(OnBodyDataOnRequestSentData data) { + } + + @Override + public void onResponseComplete(OnResponseCompleteDataOnRequestSentData data) { } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java b/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java index 309d9c33e..4cf41f201 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java @@ -126,11 +126,9 @@ public MultipartCallback getMultipartCallback() { int written; @Override public void write(AsyncHttpRequest request, final DataSink sink, final CompletedCallback completed) { - if (mParts == null) { - sink.end(); + if (mParts == null) return; - } - + Continuation c = new Continuation(new CompletedCallback() { @Override public void onCompleted(Exception ex) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java b/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java index b50b7c72d..b52fc75f2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java @@ -45,6 +45,7 @@ private void buildData() { mBodyBytes = b.toString().getBytes("ISO-8859-1"); } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java index 57e389574..40aa530cd 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java @@ -213,7 +213,7 @@ public int getCacheStoreCount() { // step 2) if this is a conditional cache request, serve it from the cache if necessary // otherwise, see if it is cacheable @Override - public void onBodyDecoder(OnBodyData data) { + public void onBodyDecoder(OnBodyDataOnRequestSentData data) { CachedSocket cached = com.koushikdutta.async.Util.getWrappedSocket(data.socket, CachedSocket.class); if (cached != null) { data.response.headers().set(SERVED_FROM, CACHE); @@ -292,7 +292,7 @@ public void onBodyDecoder(OnBodyData data) { // step 3: close up shop @Override - public void onRequestComplete(OnRequestCompleteData data) { + public void onResponseComplete(OnResponseCompleteDataOnRequestSentData data) { CacheData cacheData = data.state.get("cache-data"); if (cacheData != null && cacheData.snapshot != null) StreamUtility.closeQuietly(cacheData.snapshot); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedOutputFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedOutputFilter.java index f0f18d3b3..00d3f9e31 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedOutputFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/ChunkedOutputFilter.java @@ -25,6 +25,7 @@ public void end() { ByteBufferList fin = new ByteBufferList(); write(fin); setMaxBuffer(0); - super.end(); + // do NOT call through to super.end, as chunking is a framing protocol. + // we don't want to close the underlying transport. } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/BoundaryEmitter.java b/AndroidAsync/src/com/koushikdutta/async/http/server/BoundaryEmitter.java index 3eab5e272..e642ba448 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/BoundaryEmitter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/BoundaryEmitter.java @@ -1,6 +1,5 @@ package com.koushikdutta.async.http.server; -import android.util.Log; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.FilteredDataEmitter; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index aca08f28d..29b7fb8ed 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -94,7 +94,7 @@ void updateWindowRead(int length) { } public class SpdySocket implements AsyncSocket { - long bytesLeftInWriteWindow; + long bytesLeftInWriteWindow = AsyncSpdyConnection.this.peerSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE); WritableCallback writable; final int id; CompletedCallback closedCallback; @@ -195,9 +195,29 @@ public String charset() { return null; } + ByteBufferList writing = new ByteBufferList(); @Override public void write(ByteBufferList bb) { - System.out.println("writing!"); + int canWrite = (int)Math.min(bytesLeftInWriteWindow, AsyncSpdyConnection.this.bytesLeftInWriteWindow); + canWrite = Math.min(bb.remaining(), canWrite); + if (canWrite == 0) { + System.out.println("derp"); + return; + } + if (canWrite < bb.remaining()) { + if (writing.hasRemaining()) + throw new AssertionError("wtf"); + bb.get(writing, canWrite); + bb = writing; + } + + try { + writer.data(false, id, bb); + bytesLeftInWriteWindow -= canWrite; + } + catch (IOException e) { + throw new AssertionError(e); + } } @Override @@ -217,6 +237,12 @@ public boolean isOpen() { @Override public void end() { + try { + writer.data(true, id, writing); + } + catch (IOException e) { + throw new AssertionError(e); + } } @Override diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/Spdy3.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Spdy3.java index 35f4e6e3b..ddea2f2f3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/Spdy3.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/Spdy3.java @@ -167,12 +167,12 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { } }; + ByteBufferList partial = new ByteBufferList(); private final DataCallback onDataFrame = new DataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { int toRead = Math.min(bb.remaining(), length); if (toRead < bb.remaining()) { - ByteBufferList partial = new ByteBufferList(); bb.get(partial, toRead); bb = partial; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index 1ac43d5d8..b5a608ca4 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -16,6 +16,7 @@ import com.koushikdutta.async.http.Headers; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.Protocol; +import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.util.Charsets; import java.lang.reflect.Field; @@ -39,6 +40,94 @@ public void configureEngine(SSLEngine engine, String host, int port) { }); } + private void configure(SSLEngine engine, String host, int port) { + if (!initialized) { + initialized = true; + try { + peerHost = engine.getClass().getSuperclass().getDeclaredField("peerHost"); + peerPort = engine.getClass().getSuperclass().getDeclaredField("peerPort"); + sslParameters = engine.getClass().getDeclaredField("sslParameters"); + npnProtocols = sslParameters.getType().getDeclaredField("npnProtocols"); + alpnProtocols = sslParameters.getType().getDeclaredField("alpnProtocols"); + useSni = sslParameters.getType().getDeclaredField("useSni"); + sslNativePointer = engine.getClass().getDeclaredField("sslNativePointer"); + String nativeCryptoName = sslParameters.getType().getPackage().getName() + ".NativeCrypto"; + nativeGetNpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) + .getDeclaredMethod("SSL_get_npn_negotiated_protocol", long.class); + nativeGetAlpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) + .getDeclaredMethod("SSL_get0_alpn_selected", long.class); + + peerHost.setAccessible(true); + peerPort.setAccessible(true); + sslParameters.setAccessible(true); + npnProtocols.setAccessible(true); + alpnProtocols.setAccessible(true); + useSni.setAccessible(true); + sslNativePointer.setAccessible(true); + nativeGetNpnNegotiatedProtocol.setAccessible(true); + nativeGetAlpnNegotiatedProtocol.setAccessible(true); + } + catch (Exception e) { + sslParameters = null; + npnProtocols = null; + alpnProtocols = null; + useSni = null; + sslNativePointer = null; + nativeGetNpnNegotiatedProtocol = null; + nativeGetAlpnNegotiatedProtocol = null; + } + } + + if (sslParameters != null) { + try { + byte[] protocols = concatLengthPrefixed( + Protocol.HTTP_1_1, + Protocol.SPDY_3 + ); + + peerHost.set(engine, host); + peerPort.set(engine, port); + Object sslp = sslParameters.get(engine); +// npnProtocols.set(sslp, protocols); + alpnProtocols.set(sslp, protocols); + useSni.set(sslp, true); + } + catch (Exception e ) { + e.printStackTrace(); + } + } + } + + @Override + protected SSLEngine createConfiguredSSLEngine(String host, int port) { + SSLContext sslContext = getSSLContext(); + SSLEngine sslEngine = sslContext.createSSLEngine(); + + for (AsyncSSLEngineConfigurator configurator : engineConfigurators) { + configurator.configureEngine(sslEngine, host, port); + } + + return sslEngine; + } + + boolean initialized; + Field peerHost; + Field peerPort; + Field sslParameters; + Field npnProtocols; + Field alpnProtocols; + Field sslNativePointer; + Field useSni; + Method nativeGetNpnNegotiatedProtocol; + Method nativeGetAlpnNegotiatedProtocol; + Hashtable connections = new Hashtable(); + + @Override + public void setSSLContext(SSLContext sslContext) { + super.setSSLContext(sslContext); + initialized = false; + } + static byte[] concatLengthPrefixed(Protocol... protocols) { ByteBuffer result = ByteBuffer.allocate(8192); for (Protocol protocol: protocols) { @@ -80,6 +169,7 @@ public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { callback.onConnectCompleted(null, socket); return; } + data.protocol = protoString; final AsyncSpdyConnection connection = new AsyncSpdyConnection(socket, Protocol.get(protoString)); connection.sendConnectionPreface(); @@ -120,15 +210,44 @@ private void newSocket(GetSocketData data, final AsyncSpdyConnection connection, } } - AsyncSpdyConnection.SpdySocket spdy = connection.newStream(headers, false, true); + data.request.logv("\n" + data.request); + AsyncSpdyConnection.SpdySocket spdy = connection.newStream(headers, data.request.getBody() != null, true); callback.onConnectCompleted(null, spdy); } @Override - public boolean exchangeHeaders(final ExchangeHeaderData data) { + public Cancellable getSocket(GetSocketData data) { + final Uri uri = data.request.getUri(); + final int port = getSchemePort(data.request.getUri()); + if (port == -1) { + return null; + } + + // can we use an existing connection to satisfy this, or do we need a new one? + String host = uri.getHost(); + AsyncSpdyConnection conn = connections.get(host); + if (conn == null || !conn.socket.isOpen()) { + connections.remove(host); + return super.getSocket(data); + } + + newSocket(data, conn, data.connectCallback); + + SimpleCancellable ret = new SimpleCancellable(); + ret.setComplete(); + return ret; + } + + @Override + public boolean exchangeHeaders(final OnExchangeHeaderData data) { if (!(data.socket instanceof AsyncSpdyConnection.SpdySocket)) return false; + AsyncHttpRequestBody requestBody = data.request.getBody(); + if (requestBody != null) { + data.response.sink(data.socket); + } + // headers were already sent as part of the socket being opened. data.sendHeadersCallback.onCompleted(null); @@ -162,114 +281,12 @@ public void onCompleted(Exception e, Headers result) { return true; } - private void configure(SSLEngine engine, String host, int port) { - if (!initialized) { - initialized = true; - try { - peerHost = engine.getClass().getSuperclass().getDeclaredField("peerHost"); - peerPort = engine.getClass().getSuperclass().getDeclaredField("peerPort"); - sslParameters = engine.getClass().getDeclaredField("sslParameters"); - npnProtocols = sslParameters.getType().getDeclaredField("npnProtocols"); - alpnProtocols = sslParameters.getType().getDeclaredField("alpnProtocols"); - useSni = sslParameters.getType().getDeclaredField("useSni"); - sslNativePointer = engine.getClass().getDeclaredField("sslNativePointer"); - String nativeCryptoName = sslParameters.getType().getPackage().getName() + ".NativeCrypto"; - nativeGetNpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) - .getDeclaredMethod("SSL_get_npn_negotiated_protocol", long.class); - nativeGetAlpnNegotiatedProtocol = Class.forName(nativeCryptoName, true, sslParameters.getType().getClassLoader()) - .getDeclaredMethod("SSL_get0_alpn_selected", long.class); - - peerHost.setAccessible(true); - peerPort.setAccessible(true); - sslParameters.setAccessible(true); - npnProtocols.setAccessible(true); - alpnProtocols.setAccessible(true); - useSni.setAccessible(true); - sslNativePointer.setAccessible(true); - nativeGetNpnNegotiatedProtocol.setAccessible(true); - nativeGetAlpnNegotiatedProtocol.setAccessible(true); - } - catch (Exception e) { - sslParameters = null; - npnProtocols = null; - alpnProtocols = null; - useSni = null; - sslNativePointer = null; - nativeGetNpnNegotiatedProtocol = null; - nativeGetAlpnNegotiatedProtocol = null; - } - } - - if (sslParameters != null) { - try { - byte[] protocols = concatLengthPrefixed( - Protocol.HTTP_1_1, - Protocol.SPDY_3 - ); - - peerHost.set(engine, host); - peerPort.set(engine, port); - Object sslp = sslParameters.get(engine); -// npnProtocols.set(sslp, protocols); - alpnProtocols.set(sslp, protocols); - useSni.set(sslp, true); - } - catch (Exception e ) { - e.printStackTrace(); - } - } - } - @Override - protected SSLEngine createConfiguredSSLEngine(String host, int port) { - SSLContext sslContext = getSSLContext(); - SSLEngine sslEngine = sslContext.createSSLEngine(); - - for (AsyncSSLEngineConfigurator configurator : engineConfigurators) { - configurator.configureEngine(sslEngine, host, port); - } - - return sslEngine; - } - - boolean initialized; - Field peerHost; - Field peerPort; - Field sslParameters; - Field npnProtocols; - Field alpnProtocols; - Field sslNativePointer; - Field useSni; - Method nativeGetNpnNegotiatedProtocol; - Method nativeGetAlpnNegotiatedProtocol; - Hashtable connections = new Hashtable(); - - @Override - public void setSSLContext(SSLContext sslContext) { - super.setSSLContext(sslContext); - initialized = false; - } - - @Override - public Cancellable getSocket(GetSocketData data) { - final Uri uri = data.request.getUri(); - final int port = getSchemePort(data.request.getUri()); - if (port == -1) { - return null; - } - - // can we use an existing connection to satisfy this, or do we need a new one? - String host = uri.getHost(); - AsyncSpdyConnection conn = connections.get(host); - if (conn == null || !conn.socket.isOpen()) { - connections.remove(host); - return super.getSocket(data); - } - - newSocket(data, conn, data.connectCallback); + public void onRequestSent(OnRequestSentData data) { + if (!(data.socket instanceof AsyncSpdyConnection.SpdySocket)) + return; - SimpleCancellable ret = new SimpleCancellable(); - ret.setComplete(); - return ret; + if (data.request.getBody() != null) + data.response.sink().end(); } } \ No newline at end of file From 06987e72af6bad8d4c88a8b587218375f1955810 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 20:33:28 -0700 Subject: [PATCH 061/399] fix all the tests. --- .../async/http/cache/ResponseHeaders.java | 2 +- .../koushikdutta/async/test/CacheTests.java | 8 +- .../async/test/ConscryptTests.java | 84 +------------- .../koushikdutta/async/test/FileTests.java | 6 +- .../koushikdutta/async/test/Handshake.java | 106 ------------------ .../async/test/HttpClientTests.java | 15 ++- .../async/test/MultipartTests.java | 5 +- .../koushikdutta/async/test/OkHttpTest.java | 89 --------------- 8 files changed, 19 insertions(+), 296 deletions(-) delete mode 100644 AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java delete mode 100644 AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java diff --git a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseHeaders.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseHeaders.java index 327564714..d71cf9df9 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseHeaders.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseHeaders.java @@ -160,7 +160,7 @@ public ResponseHeaders(Uri uri, RawHeaders headers) { varyFields = new TreeSet(String.CASE_INSENSITIVE_ORDER); } for (String varyField : value.split(",")) { - varyFields.add(varyField.trim()); + varyFields.add(varyField.trim().toLowerCase()); } } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) { contentEncoding = value; diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/CacheTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/CacheTests.java index 78f330126..2edd1ae08 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/CacheTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/CacheTests.java @@ -1,6 +1,6 @@ package com.koushikdutta.async.test; -import android.os.Environment; +import android.test.AndroidTestCase; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncServerSocket; @@ -13,18 +13,16 @@ import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.HttpServerRequestCallback; -import junit.framework.TestCase; - import java.io.File; import java.util.Date; /** * Created by koush on 6/13/13. */ -public class CacheTests extends TestCase { +public class CacheTests extends AndroidTestCase { public void testMaxAgePrivate() throws Exception { AsyncHttpClient client = new AsyncHttpClient(AsyncServer.getDefault()); - ResponseCacheMiddleware cache = ResponseCacheMiddleware.addCache(client, new File(Environment.getExternalStorageDirectory(), "AndroidAsyncTest"), 1024 * 1024 * 10); + ResponseCacheMiddleware cache = ResponseCacheMiddleware.addCache(client, new File(getContext().getFilesDir(), "AndroidAsyncTest"), 1024 * 1024 * 10); AsyncHttpServer httpServer = new AsyncHttpServer(); try { httpServer.get("/uname/(.*)", new HttpServerRequestCallback() { diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java index cd5014883..2bcfd5a78 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java @@ -17,24 +17,11 @@ package com.koushikdutta.async.test; -import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.spdy.ErrorCode; -import com.koushikdutta.async.http.spdy.FrameReader; -import com.koushikdutta.async.http.spdy.Header; -import com.koushikdutta.async.http.spdy.HeadersMode; -import com.koushikdutta.async.http.spdy.Settings; -import com.koushikdutta.async.http.spdy.Spdy3; -import com.koushikdutta.async.http.spdy.okio.BufferedSource; -import com.koushikdutta.async.http.spdy.okio.ByteString; -import com.koushikdutta.async.http.spdy.okio.Okio; - import junit.framework.TestCase; import org.conscrypt.OpenSSLEngineImpl; import org.conscrypt.OpenSSLProvider; -import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Field; @@ -44,7 +31,6 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.security.Security; -import java.util.List; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -193,74 +179,6 @@ else if (handshakeStatus == SSLEngineResult.HandshakeStatus.NEED_TASK) { System.out.println("negotiated protocol was: " + protoString); assertEquals(protoString, "spdy/3.1"); - dummy.clear(); - SSLEngineResult res = engine.unwrap(unwrap, dummy); - dummy.flip(); - byte[] frame = new byte[dummy.remaining()]; - dummy.get(frame ); - Spdy3 spdy3 = new Spdy3(); - BufferedSource source = Okio.buffer(Okio.source(new ByteArrayInputStream(frame))); - FrameReader frameReader = spdy3.newReader(source, true); - ByteBufferList bb = new ByteBufferList(ByteBuffer.wrap(frame)); - assertTrue(frameReader.canProcessFrame(bb)); - - frameReader.nextFrame(new FrameReader.Handler() { - @Override - public void data(boolean inFinished, int streamId, BufferedSource source, int length) throws IOException { - - } - - @Override - public void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId, List
headerBlock, HeadersMode headersMode) { - - } - - @Override - public void rstStream(int streamId, ErrorCode errorCode) { - - } - - @Override - public void settings(boolean clearPrevious, Settings settings) { - - } - - @Override - public void ackSettings() { - - } - - @Override - public void ping(boolean ack, int payload1, int payload2) { - - } - - @Override - public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) { - - } - - @Override - public void windowUpdate(int streamId, long windowSizeIncrement) { - - } - - @Override - public void priority(int streamId, int streamDependency, int weight, boolean exclusive) { - - } - - @Override - public void pushPromise(int streamId, int promisedStreamId, List
requestHeaders) throws IOException { - - } - - @Override - public void alternateService(int streamId, String origin, ByteString protocol, String host, int port, long maxAge) { - - } - }); - - + socket.close(); } } diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/FileTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/FileTests.java index 1d372397a..4642bd2f5 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/FileTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/FileTests.java @@ -1,5 +1,7 @@ package com.koushikdutta.async.test; +import android.test.AndroidTestCase; + import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.FileDataEmitter; import com.koushikdutta.async.future.Future; @@ -16,11 +18,11 @@ /** * Created by koush on 5/22/13. */ -public class FileTests extends TestCase { +public class FileTests extends AndroidTestCase { public static final long TIMEOUT = 1000L; public void testFileDataEmitter() throws Exception { final Semaphore semaphore = new Semaphore(0); - File f = new File("/sdcard/test.txt"); + File f = getContext().getFileStreamPath("test.txt"); StreamUtility.writeFile(f, "hello world"); FileDataEmitter fdm = new FileDataEmitter(AsyncServer.getDefault(), f); final Md5 md5 = Md5.createInstance(); diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java b/AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java deleted file mode 100644 index 597436b6a..000000000 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/Handshake.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.koushikdutta.async.test; - -import com.koushikdutta.async.http.spdy.Util; - -import java.security.Principal; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.util.Collections; -import java.util.List; - -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; - -/** - * A record of a TLS handshake. For HTTPS clients, the client is local - * and the remote server is its peer. - * - *

This value object describes a completed handshake. Use {@link - * javax.net.ssl.SSLSocketFactory} to set policy for new handshakes. - */ -public final class Handshake { - private final String cipherSuite; - private final List peerCertificates; - private final List localCertificates; - - private Handshake( - String cipherSuite, List peerCertificates, List localCertificates) { - this.cipherSuite = cipherSuite; - this.peerCertificates = peerCertificates; - this.localCertificates = localCertificates; - } - - public static Handshake get(SSLSession session) { - String cipherSuite = session.getCipherSuite(); - if (cipherSuite == null) throw new IllegalStateException("cipherSuite == null"); - - Certificate[] peerCertificates; - try { - peerCertificates = session.getPeerCertificates(); - } catch (SSLPeerUnverifiedException ignored) { - peerCertificates = null; - } - List peerCertificatesList = peerCertificates != null - ? Util.immutableList(peerCertificates) - : Collections.emptyList(); - - Certificate[] localCertificates = session.getLocalCertificates(); - List localCertificatesList = localCertificates != null - ? Util.immutableList(localCertificates) - : Collections.emptyList(); - - return new Handshake(cipherSuite, peerCertificatesList, localCertificatesList); - } - - public static Handshake get( - String cipherSuite, List peerCertificates, List localCertificates) { - if (cipherSuite == null) throw new IllegalArgumentException("cipherSuite == null"); - return new Handshake(cipherSuite, Util.immutableList(peerCertificates), - Util.immutableList(localCertificates)); - } - - /** Returns a cipher suite name like "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA". */ - public String cipherSuite() { - return cipherSuite; - } - - /** Returns a possibly-empty list of certificates that identify the remote peer. */ - public List peerCertificates() { - return peerCertificates; - } - - /** Returns the remote peer's principle, or null if that peer is anonymous. */ - public Principal peerPrincipal() { - return !peerCertificates.isEmpty() - ? ((X509Certificate) peerCertificates.get(0)).getSubjectX500Principal() - : null; - } - - /** Returns a possibly-empty list of certificates that identify this peer. */ - public List localCertificates() { - return localCertificates; - } - - /** Returns the local principle, or null if this peer is anonymous. */ - public Principal localPrincipal() { - return !localCertificates.isEmpty() - ? ((X509Certificate) localCertificates.get(0)).getSubjectX500Principal() - : null; - } - - @Override public boolean equals(Object other) { - if (!(other instanceof Handshake)) return false; - Handshake that = (Handshake) other; - return cipherSuite.equals(that.cipherSuite) - && peerCertificates.equals(that.peerCertificates) - && localCertificates.equals(that.localCertificates); - } - - @Override public int hashCode() { - int result = 17; - result = 31 * result + cipherSuite.hashCode(); - result = 31 * result + peerCertificates.hashCode(); - result = 31 * result + localCertificates.hashCode(); - return result; - } -} \ No newline at end of file diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java index 5be77f90e..4ba7c7d30 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/HttpClientTests.java @@ -2,6 +2,7 @@ import android.net.Uri; import android.os.Environment; +import android.test.AndroidTestCase; import android.text.TextUtils; import android.util.Log; @@ -20,17 +21,14 @@ import com.koushikdutta.async.http.AsyncHttpPost; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.AsyncHttpResponse; -import com.koushikdutta.async.http.cache.ResponseCacheMiddleware; import com.koushikdutta.async.http.body.JSONObjectBody; +import com.koushikdutta.async.http.cache.ResponseCacheMiddleware; import com.koushikdutta.async.http.callback.HttpConnectCallback; -import com.koushikdutta.async.http.server.AsyncHttpServer; import com.koushikdutta.async.http.server.AsyncHttpServerRequest; import com.koushikdutta.async.http.server.AsyncHttpServerResponse; import com.koushikdutta.async.http.server.AsyncProxyServer; -import com.koushikdutta.async.http.server.HttpServerRequestCallback; import junit.framework.Assert; -import junit.framework.TestCase; import org.json.JSONObject; @@ -41,7 +39,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -public class HttpClientTests extends TestCase { +public class HttpClientTests extends AndroidTestCase { AsyncHttpClient client; AsyncServer server = new AsyncServer(); @@ -227,7 +225,7 @@ public void onConnect(AsyncHttpResponse response) { } public void testCache() throws Exception { - ResponseCacheMiddleware cache = ResponseCacheMiddleware.addCache(client, new File(Environment.getExternalStorageDirectory(), "AndroidAsyncTest"), 1024 * 1024 * 10); + ResponseCacheMiddleware cache = ResponseCacheMiddleware.addCache(client, new File(getContext().getFilesDir(), "AndroidAsyncTest"), 1024 * 1024 * 10); try { // clear the old cache cache.clear(); @@ -245,7 +243,8 @@ public void testCache() throws Exception { Future fileFuture; public void testFileCancel() throws Exception { final Semaphore semaphore = new Semaphore(0); - fileFuture = client.executeFile(new AsyncHttpGet(github), "/sdcard/hello.txt", new AsyncHttpClient.FileCallback() { + File f = getContext().getFileStreamPath("test.txt"); + fileFuture = client.executeFile(new AsyncHttpGet(github), f.getAbsolutePath(), new AsyncHttpClient.FileCallback() { @Override public void onCompleted(Exception e, AsyncHttpResponse source, File result) { fail(); @@ -274,7 +273,7 @@ public void onCompleted(Exception e, File result) { } // Thread.sleep(1000); // assertTrue("timeout", semaphore.tryAcquire(TIMEOUT, TimeUnit.MILLISECONDS)); - assertFalse(new File("/sdcard/hello.txt").exists()); + assertFalse(f.exists()); } boolean wasProxied; diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/MultipartTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/MultipartTests.java index cace2a549..0a4a5a246 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/MultipartTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/MultipartTests.java @@ -1,6 +1,7 @@ package com.koushikdutta.async.test; import android.os.Environment; +import android.test.AndroidTestCase; import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.ByteBufferList; @@ -26,7 +27,7 @@ import java.io.FileOutputStream; import java.util.concurrent.TimeUnit; -public class MultipartTests extends TestCase { +public class MultipartTests extends AndroidTestCase { AsyncHttpServer httpServer; @Override @@ -81,7 +82,7 @@ protected void tearDown() throws Exception { } public void testUpload() throws Exception { - File dummy = new File(Environment.getExternalStorageDirectory(), "AndroidAsync/dummy.txt"); + File dummy = getContext().getFileStreamPath("dummy.txt"); final String FIELD_VAL = "bar"; dummy.getParentFile().mkdirs(); FileOutputStream fout = new FileOutputStream(dummy); diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java b/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java deleted file mode 100644 index 48fbf5b69..000000000 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/OkHttpTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.koushikdutta.async.test; - - -import android.test.AndroidTestCase; - -import com.koushikdutta.async.ByteBufferList; -import com.koushikdutta.async.http.Protocol; -import com.koushikdutta.async.util.Charsets; - -import org.conscrypt.OpenSSLProvider; - -import java.lang.reflect.Method; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.nio.ByteBuffer; -import java.security.Security; - -import javax.net.SocketFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocket; - -public class OkHttpTest extends AndroidTestCase { - public void testOkHttp() throws Exception { -// Context context = getContext().getApplicationContext(); -// Context gms = context.createPackageContext("com.google.android.gms", Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); -// gms -// .getClassLoader() -// .loadClass("com.google.android.gms.common.security.ProviderInstallerImpl") -// .getMethod("insertProvider", Context.class) -// .invoke(null, context); - Security.insertProviderAt(new OpenSSLProvider("MyNameBlah"), 1); - - Class openSslSocketClass; - Method setUseSessionTickets; - Method setHostname; - openSslSocketClass = Class.forName("org.conscrypt.OpenSSLSocketImpl"); - setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class); - setHostname = openSslSocketClass.getMethod("setHostname", String.class); - Method trafficStatsTagSocket = null; - Method trafficStatsUntagSocket = null; - Class trafficStats = Class.forName("android.net.TrafficStats"); - trafficStatsTagSocket = trafficStats.getMethod("tagSocket", Socket.class); - trafficStatsUntagSocket = trafficStats.getMethod("untagSocket", Socket.class); - - // Attempt to find Android 4.1+ APIs. - Method setNpnProtocols = null; - Method getNpnSelectedProtocol = null; - setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class); - getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol"); - - -// Platform p = Platform.get(); - - SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.init(null, null, null); - Socket socket = SocketFactory.getDefault().createSocket(); - socket.connect(new InetSocketAddress("www.google.com", 443)); - socket = ctx.getSocketFactory().createSocket(socket, "www.google.com", 443, true); - SSLSocket sslSocket = (SSLSocket) socket; - - setUseSessionTickets.invoke(sslSocket, true); - setHostname.invoke(sslSocket, "www.google.com"); - setNpnProtocols.invoke(sslSocket, new Object[] { concatLengthPrefixed(Protocol.HTTP_1_1, Protocol.SPDY_3) }); - - - sslSocket.startHandshake(); - Handshake handshake = Handshake.get(sslSocket.getSession()); - - String proto = new String((byte[])getNpnSelectedProtocol.invoke(sslSocket)); - -// InputStream is = sslSocket.getInputStream(); -// StreamUtility.eat(is); - - System.out.println(proto); - } - - static byte[] concatLengthPrefixed(Protocol... protocols) { - ByteBuffer result = ByteBuffer.allocate(8192); - for (Protocol protocol: protocols) { - if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for NPN. - result.put((byte) protocol.toString().length()); - result.put(protocol.toString().getBytes(Charsets.UTF_8)); - } - result.flip(); - byte[] ret = new ByteBufferList(result).getAllByteArray(); - return ret; - } - -} \ No newline at end of file From cfaf22a7a260b60f6814545cede08237e60206c1 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 28 Jul 2014 22:39:08 -0700 Subject: [PATCH 062/399] remove conscrypt dependency. not necessary with alpn not exhibiting the same handshake bug as npn. --- AndroidAsync/AndroidAsync-AndroidAsync.iml | 6 ------ AndroidAsync/build.gradle | 4 ++-- .../async/http/spdy/SpdyMiddleware.java | 11 +++++++--- .../async/test/ConscryptTests.java | 21 ++++++++++++------- 4 files changed, 23 insertions(+), 19 deletions(-) diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index f88736b56..1cdbd092c 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -60,12 +60,6 @@ - - - - - - diff --git a/AndroidAsync/build.gradle b/AndroidAsync/build.gradle index 77970b0f0..05d82cad1 100644 --- a/AndroidAsync/build.gradle +++ b/AndroidAsync/build.gradle @@ -16,8 +16,8 @@ android { jniLibs.srcDirs = ['libs/'] java.srcDirs=['src/' - , '../conscrypt/' - , '../compat/' +// , '../conscrypt/' +// , '../compat/' ] } androidTest.java.srcDirs=['test/src/'] diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index b5a608ca4..c4bbe52f5 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -1,6 +1,7 @@ package com.koushikdutta.async.http.spdy; import android.net.Uri; +import android.text.TextUtils; import com.koushikdutta.async.AsyncSSLSocket; import com.koushikdutta.async.AsyncSSLSocketWrapper; @@ -141,9 +142,13 @@ static byte[] concatLengthPrefixed(Protocol... protocols) { } private static String requestPath(Uri uri) { - String pathAndQuery = uri.getPath(); - if (pathAndQuery == null) return "/"; - if (!pathAndQuery.startsWith("/")) return "/" + pathAndQuery; + String pathAndQuery = uri.getEncodedPath(); + if (pathAndQuery == null) + pathAndQuery = "/"; + else if (!pathAndQuery.startsWith("/")) + pathAndQuery = "/" + pathAndQuery; + if (!TextUtils.isEmpty(uri.getEncodedQuery())) + pathAndQuery += "?" + uri.getEncodedQuery(); return pathAndQuery; } diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java index 2bcfd5a78..ac18eadee 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/ConscryptTests.java @@ -17,10 +17,8 @@ package com.koushikdutta.async.test; -import junit.framework.TestCase; - -import org.conscrypt.OpenSSLEngineImpl; -import org.conscrypt.OpenSSLProvider; +import android.content.Context; +import android.test.AndroidTestCase; import java.io.InputStream; import java.io.OutputStream; @@ -30,7 +28,6 @@ import java.net.Socket; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.security.Security; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -39,7 +36,7 @@ /** * Created by koush on 7/15/14. */ -public class ConscryptTests extends TestCase { +public class ConscryptTests extends AndroidTestCase { boolean initialized; Field peerHost; Field peerPort; @@ -104,11 +101,19 @@ static byte[] concatLengthPrefixed(String... protocols) { } public void testConscryptSSLEngineNPNHandshakeBug() throws Exception { - Security.insertProviderAt(new OpenSSLProvider("MyNameBlah"), 1); +// Security.insertProviderAt(new OpenSSLProvider("MyNameBlah"), 1); + + Context gms = getContext().createPackageContext("com.google.android.gms", Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); + gms + .getClassLoader() + .loadClass("com.google.android.gms.common.security.ProviderInstallerImpl") + .getMethod("insertProvider", Context.class) + .invoke(null, getContext()); + SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(null, null, null); - OpenSSLEngineImpl engine = (OpenSSLEngineImpl)ctx.createSSLEngine(); + SSLEngine engine = ctx.createSSLEngine(); configure(engine, "www.google.com", 443); engine.setUseClientMode(true); engine.beginHandshake(); From 1d505a845934cf5ba7cb02fb8749a39fd57e73a0 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Tue, 29 Jul 2014 13:41:44 -0700 Subject: [PATCH 063/399] remove defaultConfig stuff. --- AndroidAsync/build.gradle | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/AndroidAsync/build.gradle b/AndroidAsync/build.gradle index 05d82cad1..02e423597 100644 --- a/AndroidAsync/build.gradle +++ b/AndroidAsync/build.gradle @@ -25,15 +25,15 @@ android { androidTest.assets.srcDirs=['test/assets/'] } -// lintOptions { -// abortOnError false -// } - - defaultConfig { - targetSdkVersion 21 - minSdkVersion 9 + lintOptions { + abortOnError false } +// defaultConfig { +// targetSdkVersion 21 +// minSdkVersion 9 +// } + compileSdkVersion 19 buildToolsVersion "20.0.0" } From e03f1f8be424508823c07b8741b447bc9b683ea9 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Tue, 29 Jul 2014 18:34:19 -0700 Subject: [PATCH 064/399] watch for exceptions when setting the proxy --- .../src/com/koushikdutta/async/http/AsyncHttpClient.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index 49c7de506..956fb26ac 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -81,7 +81,14 @@ private static void setupAndroidProxy(AsyncHttpRequest request) { if (request.proxyHost != null) return; - List proxies = ProxySelector.getDefault().select(URI.create(request.getUri().toString())); + List proxies; + try { + proxies = ProxySelector.getDefault().select(URI.create(request.getUri().toString())); + } + catch (Exception e) { + // uri parsing craps itself sometimes. + return; + } if (proxies.isEmpty()) return; Proxy proxy = proxies.get(0); From aa77649d29788a755958f407d542be31f1ccd3b0 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sun, 3 Aug 2014 15:13:22 -0700 Subject: [PATCH 065/399] tolerate no response message --- .../com/koushikdutta/async/http/HttpTransportMiddleware.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java index 6659193e1..219519dad 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java @@ -53,14 +53,14 @@ else if (!"\r".equals(s)) { } else { String[] parts = statusLine.split(" ", 3); - if (parts.length != 3) + if (parts.length < 2) throw new Exception(new IOException("Not HTTP")); data.response.headers(mRawHeaders); String protocol = parts[0]; data.response.protocol(protocol); data.response.code(Integer.parseInt(parts[1])); - data.response.message(parts[2]); + data.response.message(parts.length == 3 ? parts[2] : ""); data.receiveHeadersCallback.onCompleted(null); // socket may get detached after headers (websocket) From 86079517f2a9b1c5e509e504c2a8ff96bf3fbc76 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sun, 3 Aug 2014 15:35:58 -0700 Subject: [PATCH 066/399] Some HTTP servers are sending only newline instead of CRLF. --- .../com/koushikdutta/async/http/AsyncHttpResponseImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index 2b77b568e..70f10f77b 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -1,5 +1,7 @@ package com.koushikdutta.async.http; +import android.text.TextUtils; + import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; @@ -109,10 +111,11 @@ public void onCompleted(Exception error) { @Override public void onStringAvailable(String s) { try { + s = s.trim(); if (mRawHeaders.getStatusLine() == null) { mRawHeaders.setStatusLine(s); } - else if (!"\r".equals(s)) { + else if (!TextUtils.isEmpty(s)) { mRawHeaders.addLine(s); } else { From 9808e0391b8f9bff7247ee0f7366caeb77230406 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sun, 3 Aug 2014 15:40:02 -0700 Subject: [PATCH 067/399] missing file --- .../com/koushikdutta/async/http/HttpTransportMiddleware.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java index 219519dad..9dacb55de 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/HttpTransportMiddleware.java @@ -1,5 +1,7 @@ package com.koushikdutta.async.http; +import android.text.TextUtils; + import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.LineEmitter; @@ -45,10 +47,11 @@ public boolean exchangeHeaders(final OnExchangeHeaderData data) { @Override public void onStringAvailable(String s) { try { + s = s.trim(); if (statusLine == null) { statusLine = s; } - else if (!"\r".equals(s)) { + else if (!TextUtils.isEmpty(s)) { mRawHeaders.addLine(s); } else { From 64a3b995009f21e42569b46f7f49dd7ee7f48e03 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Mon, 4 Aug 2014 21:00:01 -0700 Subject: [PATCH 068/399] Enable Content-Encoding for spdy. Disable using SPDY if sending a request body. --- .../async/http/spdy/AsyncSpdyConnection.java | 4 ++ .../async/http/spdy/SpdyMiddleware.java | 47 ++++++++++++++----- .../async/http/spdy/SpdyTransport.java | 4 -- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java index 29b7fb8ed..806968bd5 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/AsyncSpdyConnection.java @@ -105,6 +105,10 @@ public class SpdySocket implements AsyncSocket { boolean isOpen = true; int totalWindowRead; + public AsyncSpdyConnection getConnection() { + return AsyncSpdyConnection.this; + } + public SimpleFuture> headers() { return headers; } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index c4bbe52f5..211f06b90 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java @@ -6,15 +6,19 @@ import com.koushikdutta.async.AsyncSSLSocket; import com.koushikdutta.async.AsyncSSLSocketWrapper; import com.koushikdutta.async.ByteBufferList; +import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.future.SimpleCancellable; import com.koushikdutta.async.future.TransformFuture; import com.koushikdutta.async.http.AsyncHttpClient; +import com.koushikdutta.async.http.AsyncHttpClientMiddleware; +import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.AsyncSSLEngineConfigurator; import com.koushikdutta.async.http.AsyncSSLSocketMiddleware; import com.koushikdutta.async.http.Headers; +import com.koushikdutta.async.http.HttpUtil; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.Protocol; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; @@ -174,7 +178,6 @@ public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { callback.onConnectCompleted(null, socket); return; } - data.protocol = protoString; final AsyncSpdyConnection connection = new AsyncSpdyConnection(socket, Protocol.get(protoString)); connection.sendConnectionPreface(); @@ -191,11 +194,25 @@ public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) { } private void newSocket(GetSocketData data, final AsyncSpdyConnection connection, final ConnectCallback callback) { - data.request.logv("using spdy connection"); + final AsyncHttpRequest request = data.request; + request.logv("using spdy connection"); + + data.protocol = connection.protocol.toString(); + + final AsyncHttpRequestBody requestBody = data.request.getBody(); + + // this causes app engine to shit a brick, but if it is missing, + // drive shits the bed +// if (requestBody != null) { +// if (requestBody.length() >= 0) { +// request.getHeaders().set("Content-Length", String.valueOf(requestBody.length())); +// } +// } + final ArrayList

headers = new ArrayList
(); - headers.add(new Header(Header.TARGET_METHOD, data.request.getMethod())); - headers.add(new Header(Header.TARGET_PATH, requestPath(data.request.getUri()))); - String host = data.request.getHeaders().get("Host"); + headers.add(new Header(Header.TARGET_METHOD, request.getMethod())); + headers.add(new Header(Header.TARGET_PATH, requestPath(request.getUri()))); + String host = request.getHeaders().get("Host"); if (Protocol.SPDY_3 == connection.protocol) { headers.add(new Header(Header.VERSION, "HTTP/1.1")); headers.add(new Header(Header.TARGET_HOST, host)); @@ -204,9 +221,9 @@ private void newSocket(GetSocketData data, final AsyncSpdyConnection connection, } else { throw new AssertionError(); } - headers.add(new Header(Header.TARGET_SCHEME, data.request.getUri().getScheme())); + headers.add(new Header(Header.TARGET_SCHEME, request.getUri().getScheme())); - Multimap mm = data.request.getHeaders().getMultiMap(); + final Multimap mm = request.getHeaders().getMultiMap(); for (String key: mm.keySet()) { if (SpdyTransport.isProhibitedHeader(connection.protocol, key)) continue; @@ -215,8 +232,9 @@ private void newSocket(GetSocketData data, final AsyncSpdyConnection connection, } } - data.request.logv("\n" + data.request); - AsyncSpdyConnection.SpdySocket spdy = connection.newStream(headers, data.request.getBody() != null, true); + + request.logv("\n" + request); + final AsyncSpdyConnection.SpdySocket spdy = connection.newStream(headers, requestBody != null, true); callback.onConnectCompleted(null, spdy); } @@ -228,6 +246,12 @@ public Cancellable getSocket(GetSocketData data) { return null; } + // TODO: figure out why POST does not work if sending content-length header + // see above regarding app engine comment as to why: drive requires content-length + // but app engine sends a GO_AWAY if it sees a content-length... + if (data.request.getBody() != null) + return null; + // can we use an existing connection to satisfy this, or do we need a new one? String host = uri.getHost(); AsyncSpdyConnection conn = connections.get(host); @@ -246,7 +270,7 @@ public Cancellable getSocket(GetSocketData data) { @Override public boolean exchangeHeaders(final OnExchangeHeaderData data) { if (!(data.socket instanceof AsyncSpdyConnection.SpdySocket)) - return false; + return super.exchangeHeaders(data); AsyncHttpRequestBody requestBody = data.request.getBody(); if (requestBody != null) { @@ -280,7 +304,8 @@ protected void transform(List
result) throws Exception { @Override public void onCompleted(Exception e, Headers result) { data.receiveHeadersCallback.onCompleted(e); - data.response.emitter(spdySocket); + DataEmitter emitter = HttpUtil.getBodyDecoder(spdySocket, spdySocket.getConnection().protocol, result, false); + data.response.emitter(emitter); } }); return true; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java index a2e3029e2..fe07e7887 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyTransport.java @@ -25,10 +25,6 @@ final class SpdyTransport { /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */ private static final List SPDY_3_PROHIBITED_HEADERS = Util.immutableList( - "accept-encoding", - "user-agent", - "accept", - "connection", "host", "keep-alive", From b5ad587bf3611a4e2cc63967c0889106f50cd917 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 6 Aug 2014 19:19:00 -0700 Subject: [PATCH 069/399] move NullDataCallback. change how socket ownership works. fix possible bugs around that. throw assertion error if connect callback is invoked twice. Switch to future? --- .../async/AsyncSSLSocketWrapper.java | 2 +- .../koushikdutta/async/NullDataCallback.java | 10 -------- .../src/com/koushikdutta/async/Util.java | 14 ++++++++--- .../async/callback/CompletedCallback.java | 7 ++++++ .../async/callback/DataCallback.java | 7 ++++++ .../async/http/AsyncHttpClient.java | 25 +++++++++++++------ .../async/http/AsyncHttpResponseImpl.java | 6 ----- .../async/http/AsyncSocketMiddleware.java | 12 ++++----- .../http/body/MultipartFormDataBody.java | 1 - .../http/cache/ResponseCacheMiddleware.java | 5 +++- .../async/http/filter/GZIPInputFilter.java | 1 - .../async/http/server/AsyncHttpServer.java | 1 - .../async/http/server/UnknownRequestBody.java | 3 +-- .../transport/WebSocketTransport.java | 4 +-- .../async/test/ByteUtilTests.java | 6 ++--- 15 files changed, 60 insertions(+), 44 deletions(-) delete mode 100644 AndroidAsync/src/com/koushikdutta/async/NullDataCallback.java diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 0f777e6ea..8760bdd38 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -411,7 +411,7 @@ private void report(Exception e) { final HandshakeCallback hs = handshakeCallback; if (hs != null) { handshakeCallback = null; - mSocket.setDataCallback(new NullDataCallback()); + mSocket.setDataCallback(new DataCallback.NullDataCallback()); mSocket.end(); mSocket.close(); hs.onHandshakeCompleted(e, null); diff --git a/AndroidAsync/src/com/koushikdutta/async/NullDataCallback.java b/AndroidAsync/src/com/koushikdutta/async/NullDataCallback.java deleted file mode 100644 index 3f9be2444..000000000 --- a/AndroidAsync/src/com/koushikdutta/async/NullDataCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.koushikdutta.async; - -import com.koushikdutta.async.callback.DataCallback; - -public class NullDataCallback implements DataCallback { - @Override - public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { - bb.recycle(); - } -} diff --git a/AndroidAsync/src/com/koushikdutta/async/Util.java b/AndroidAsync/src/com/koushikdutta/async/Util.java index 5e2666224..d9808ae59 100644 --- a/AndroidAsync/src/com/koushikdutta/async/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/Util.java @@ -130,22 +130,30 @@ public void onWriteable() { } }); - CompletedCallback wrapper = new CompletedCallback() { + final CompletedCallback wrapper = new CompletedCallback() { boolean reported; @Override public void onCompleted(Exception ex) { if (reported) return; + reported = true; + emitter.setDataCallback(null); emitter.setEndCallback(null); sink.setClosedCallback(null); sink.setWriteableCallback(null); - reported = true; callback.onCompleted(ex); } }; emitter.setEndCallback(wrapper); - sink.setClosedCallback(wrapper); + sink.setClosedCallback(new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + if (ex == null) + ex = new IOException("sink was closed before emitter ended"); + wrapper.onCompleted(ex); + } + }); } public static void stream(AsyncSocket s1, AsyncSocket s2, CompletedCallback callback) { diff --git a/AndroidAsync/src/com/koushikdutta/async/callback/CompletedCallback.java b/AndroidAsync/src/com/koushikdutta/async/callback/CompletedCallback.java index d6c034245..a5b4d64e4 100644 --- a/AndroidAsync/src/com/koushikdutta/async/callback/CompletedCallback.java +++ b/AndroidAsync/src/com/koushikdutta/async/callback/CompletedCallback.java @@ -1,5 +1,12 @@ package com.koushikdutta.async.callback; public interface CompletedCallback { + public class NullCompletedCallback implements CompletedCallback { + @Override + public void onCompleted(Exception ex) { + + } + } + public void onCompleted(Exception ex); } diff --git a/AndroidAsync/src/com/koushikdutta/async/callback/DataCallback.java b/AndroidAsync/src/com/koushikdutta/async/callback/DataCallback.java index 564e48b59..54da7ea6d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/callback/DataCallback.java +++ b/AndroidAsync/src/com/koushikdutta/async/callback/DataCallback.java @@ -5,5 +5,12 @@ public interface DataCallback { + public class NullDataCallback implements DataCallback { + @Override + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { + bb.recycle(); + } + } + public void onDataAvailable(DataEmitter emitter, ByteBufferList bb); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index 956fb26ac..943cee93f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -10,9 +10,9 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; -import com.koushikdutta.async.NullDataCallback; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; +import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.FutureCallback; @@ -141,7 +141,7 @@ public boolean cancel() { return false; if (socket != null) { - socket.setDataCallback(new NullDataCallback()); + socket.setDataCallback(new DataCallback.NullDataCallback()); socket.close(); } @@ -172,7 +172,7 @@ private void reportConnectedCompleted(FutureAsyncHttpResponse cancel, Exception if (response != null) { // the request was cancelled, so close up shop, and eat any pending data - response.setDataCallback(new NullDataCallback()); + response.setDataCallback(new DataCallback.NullDataCallback()); response.close(); } } @@ -251,8 +251,19 @@ public void run() { // 2) wait for a connect data.connectCallback = new ConnectCallback() { + boolean reported; @Override public void onConnectCompleted(Exception ex, AsyncSocket socket) { + if (reported) { + if (socket != null) { + socket.setDataCallback(new DataCallback.NullDataCallback()); + socket.setEndCallback(new CompletedCallback.NullCompletedCallback()); + socket.close(); + throw new AssertionError("double connect callback"); + } + } + reported = true; + request.logv("socket connected"); if (cancel.isCancelled()) { if (socket != null) @@ -264,14 +275,14 @@ public void onConnectCompleted(Exception ex, AsyncSocket socket) { if (cancel.timeoutRunnable != null) mServer.removeAllCallbacks(cancel.scheduled); - data.socket = socket; - cancel.socket = socket; - if (ex != null) { reportConnectedCompleted(cancel, ex, null, request, callback); return; } + data.socket = socket; + cancel.socket = socket; + executeSocket(request, redirectCount, cancel, callback, data); } }; @@ -563,7 +574,7 @@ public Future executeFile(AsyncHttpRequest req, final String filename, fin @Override public void cancelCleanup() { try { - cancel.get().setDataCallback(new NullDataCallback()); + cancel.get().setDataCallback(new DataCallback.NullDataCallback()); cancel.get().close(); } catch (Exception e) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java index cd6ffffcd..c161f3060 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponseImpl.java @@ -6,16 +6,10 @@ import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.FilteredDataEmitter; -import com.koushikdutta.async.LineEmitter; -import com.koushikdutta.async.LineEmitter.StringCallback; -import com.koushikdutta.async.NullDataCallback; -import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; -import com.koushikdutta.async.http.filter.ChunkedOutputFilter; -import java.io.IOException; import java.nio.charset.Charset; abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements AsyncSocket, AsyncHttpResponse, AsyncHttpClientMiddleware.ResponseHead { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java index 35ce3dd70..12dd28a06 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java @@ -6,10 +6,10 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; -import com.koushikdutta.async.NullDataCallback; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ConnectCallback; import com.koushikdutta.async.callback.ContinuationCallback; +import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.future.Cancellable; import com.koushikdutta.async.future.Continuation; import com.koushikdutta.async.future.SimpleCancellable; @@ -127,6 +127,8 @@ public Cancellable getSocket(final GetSocketData data) { return null; } + data.state.put("socket-owner", this); + final String lookup = computeLookup(uri, port, data.request.getProxyHost(), data.request.getProxyPort()); ConnectionInfo info = getOrCreateConnectionInfo(lookup); synchronized (AsyncSocketMiddleware.this) { @@ -139,7 +141,6 @@ public Cancellable getSocket(final GetSocketData data) { info.openCount++; - data.state.put(getClass().getCanonicalName() + ".owned", true); while (!info.sockets.isEmpty()) { IdleSocketHolder idleSocketHolder = info.sockets.pop(); @@ -239,7 +240,7 @@ public void onConnectCompleted(Exception ex, AsyncSocket socket) { } if (setComplete(null, socket)) { - data.connectCallback.onConnectCompleted(ex, socket); + data.connectCallback.onConnectCompleted(null, socket); } } })); @@ -313,7 +314,7 @@ public void onCompleted(Exception ex) { socket.setWriteableCallback(null); // should not get any data after this point... // if so, eat it and disconnect. - socket.setDataCallback(new NullDataCallback() { + socket.setDataCallback(new DataCallback.NullDataCallback() { @Override public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { super.onDataAvailable(emitter, bb); @@ -346,9 +347,8 @@ private void nextConnection(AsyncHttpRequest request) { @Override public void onResponseComplete(final OnResponseCompleteDataOnRequestSentData data) { - if (!data.state.get(getClass().getCanonicalName() + ".owned", false)) { + if (data.state.get("socket-owner") != this) return; - } try { idleSocket(data.socket); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java b/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java index 4cf41f201..0a8a26d3d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/body/MultipartFormDataBody.java @@ -5,7 +5,6 @@ import com.koushikdutta.async.DataSink; import com.koushikdutta.async.LineEmitter; import com.koushikdutta.async.LineEmitter.StringCallback; -import com.koushikdutta.async.NullDataCallback; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ContinuationCallback; import com.koushikdutta.async.callback.DataCallback; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java index 40aa530cd..ae8417332 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java @@ -173,7 +173,10 @@ public void run() { } }); cacheHitCount++; - return new SimpleCancellable(); + data.state.put("socket-owner", this); + SimpleCancellable ret = new SimpleCancellable(); + ret.setComplete(); + return ret; } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) { data.request.logi("Response may be served from conditional cache"); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java index 83beef7f7..81b9c1df8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java @@ -2,7 +2,6 @@ import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; -import com.koushikdutta.async.NullDataCallback; import com.koushikdutta.async.PushParser; import com.koushikdutta.async.PushParser.ParseCallback; import com.koushikdutta.async.callback.DataCallback; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java index 3d6566933..c18c5d596 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java @@ -13,7 +13,6 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataEmitter; -import com.koushikdutta.async.NullDataCallback; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.ListenCallback; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/UnknownRequestBody.java b/AndroidAsync/src/com/koushikdutta/async/http/server/UnknownRequestBody.java index 14a142479..a3fde6dc3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/UnknownRequestBody.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/UnknownRequestBody.java @@ -2,7 +2,6 @@ import com.koushikdutta.async.DataEmitter; import com.koushikdutta.async.DataSink; -import com.koushikdutta.async.NullDataCallback; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; import com.koushikdutta.async.callback.DataCallback; @@ -64,6 +63,6 @@ public DataEmitter getEmitter() { public void parse(DataEmitter emitter, CompletedCallback completed) { this.emitter = emitter; emitter.setEndCallback(completed); - emitter.setDataCallback(new NullDataCallback()); + emitter.setDataCallback(new DataCallback.NullDataCallback()); } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/socketio/transport/WebSocketTransport.java b/AndroidAsync/src/com/koushikdutta/async/http/socketio/transport/WebSocketTransport.java index 15928a3f3..5514ecd09 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/socketio/transport/WebSocketTransport.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/socketio/transport/WebSocketTransport.java @@ -1,8 +1,8 @@ package com.koushikdutta.async.http.socketio.transport; import com.koushikdutta.async.AsyncServer; -import com.koushikdutta.async.NullDataCallback; import com.koushikdutta.async.callback.CompletedCallback; +import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.WebSocket; public class WebSocketTransport implements SocketIOTransport { @@ -12,7 +12,7 @@ public class WebSocketTransport implements SocketIOTransport { public WebSocketTransport(WebSocket webSocket) { this.webSocket = webSocket; - this.webSocket.setDataCallback(new NullDataCallback()); + this.webSocket.setDataCallback(new DataCallback.NullDataCallback()); } @Override diff --git a/AndroidAsync/test/src/com/koushikdutta/async/test/ByteUtilTests.java b/AndroidAsync/test/src/com/koushikdutta/async/test/ByteUtilTests.java index 0d2f5df6a..386c31a4b 100644 --- a/AndroidAsync/test/src/com/koushikdutta/async/test/ByteUtilTests.java +++ b/AndroidAsync/test/src/com/koushikdutta/async/test/ByteUtilTests.java @@ -2,10 +2,10 @@ import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.FilteredDataEmitter; -import com.koushikdutta.async.NullDataCallback; import com.koushikdutta.async.PushParser; import com.koushikdutta.async.TapCallback; import com.koushikdutta.async.Util; +import com.koushikdutta.async.callback.DataCallback; import junit.framework.TestCase; @@ -23,7 +23,7 @@ public boolean isPaused() { } }; new PushParser(mock) - .until((byte)0, new NullDataCallback()) + .until((byte)0, new DataCallback.NullDataCallback()) .readInt(new PushParser.ParseCallback() { public void parsed(Integer arg) { valRead = arg; @@ -43,7 +43,7 @@ public boolean isPaused() { } }; new PushParser(mock) - .until((byte)0, new NullDataCallback()) + .until((byte)0, new DataCallback.NullDataCallback()) .readInt() .tap(new TapCallback() { public void parsed(int arg) { From 871a496436a916704173a8c61e0381724a8c0b41 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 7 Aug 2014 10:02:17 -0700 Subject: [PATCH 070/399] name future threads --- .../src/com/koushikdutta/async/future/FutureThread.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AndroidAsync/src/com/koushikdutta/async/future/FutureThread.java b/AndroidAsync/src/com/koushikdutta/async/future/FutureThread.java index b83dd47f1..ec089cc66 100644 --- a/AndroidAsync/src/com/koushikdutta/async/future/FutureThread.java +++ b/AndroidAsync/src/com/koushikdutta/async/future/FutureThread.java @@ -5,6 +5,10 @@ */ public class FutureThread extends SimpleFuture { public FutureThread(final FutureRunnable runnable) { + this(runnable, "FutureThread"); + } + + public FutureThread(final FutureRunnable runnable, String name) { new Thread(new Runnable() { @Override public void run() { From c79ad08b4994267c6d23abe8d778c12d23aea94c Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Tue, 29 Jul 2014 18:34:19 -0700 Subject: [PATCH 071/399] watch for exceptions when setting the proxy --- .../src/com/koushikdutta/async/http/AsyncHttpClient.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java index 504d5c602..0034dbe0a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpClient.java @@ -81,7 +81,14 @@ private static void setupAndroidProxy(AsyncHttpRequest request) { if (request.proxyHost != null) return; - List proxies = ProxySelector.getDefault().select(URI.create(request.getUri().toString())); + List proxies; + try { + proxies = ProxySelector.getDefault().select(URI.create(request.getUri().toString())); + } + catch (Exception e) { + // uri parsing craps itself sometimes. + return; + } if (proxies.isEmpty()) return; Proxy proxy = proxies.get(0); From db12acce66b4e64075db043a9d66b1f5eba76660 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 7 Aug 2014 14:08:13 -0700 Subject: [PATCH 072/399] fix gzip input filter --- .../koushikdutta/async/http/filter/GZIPInputFilter.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java index 81b9c1df8..8f76dbe73 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java @@ -98,14 +98,23 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { ByteBufferList.reclaim(b); } } + bb.recycle(); + done(); } }; if ((flags & FNAME) != 0) { parser.until((byte) 0, summer); + return; } if ((flags & FCOMMENT) != 0) { parser.until((byte) 0, summer); + return; } + + done(); + } + + private void done() { if (hcrc) { parser.readByteArray(2, new ParseCallback() { public void parsed(byte[] header) { From 9681d1f0701ada9cd6aba7e2ef26ed2bb1d96039 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 7 Aug 2014 14:08:13 -0700 Subject: [PATCH 073/399] fix gzip input filter --- .../koushikdutta/async/http/filter/GZIPInputFilter.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java index e1a23d014..04eb9c98f 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/GZIPInputFilter.java @@ -89,14 +89,23 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { ByteBufferList.reclaim(b); } } + bb.recycle(); + done(); } }; if ((flags & FNAME) != 0) { parser.until((byte) 0, summer); + return; } if ((flags & FCOMMENT) != 0) { parser.until((byte) 0, summer); + return; } + + done(); + } + + private void done() { if (hcrc) { parser.readByteArray(2, new ParseCallback() { public void parsed(byte[] header) { From e3e2faea978636cf5938a886a09f9578885d0f09 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Thu, 7 Aug 2014 21:15:59 -0700 Subject: [PATCH 074/399] proxy help --- AndroidAsync/AndroidAsync-AndroidAsync.iml | 20 +++++++++++++++- .../com/koushikdutta/async/http/Headers.java | 8 +++++++ .../async/http/server/AsyncHttpServer.java | 2 +- .../http/server/AsyncHttpServerResponse.java | 5 ++++ .../server/AsyncHttpServerResponseImpl.java | 23 ++++++++++++++++++- .../async/http/server/AsyncProxyServer.java | 11 +-------- 6 files changed, 56 insertions(+), 13 deletions(-) diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index 1cdbd092c..bf5ba2cad 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -56,7 +56,25 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/AndroidAsync/src/com/koushikdutta/async/http/Headers.java b/AndroidAsync/src/com/koushikdutta/async/http/Headers.java index 1b4cdc294..9724ce915 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/Headers.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/Headers.java @@ -9,6 +9,7 @@ import org.apache.http.message.BasicHeader; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -91,6 +92,13 @@ public String remove(String header) { return r.get(0); } + public Headers removeAll(Collection headers) { + for (String header: headers) { + remove(header); + } + return this; + } + public Header[] toHeaderArray() { ArrayList
ret = new ArrayList
(); for (String key: map.keySet()) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java index c18c5d596..d5db48b80 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java @@ -482,7 +482,7 @@ public void onCompleted(Exception ex) { } }); } - + private static Hashtable mCodes = new Hashtable(); static { mCodes.put(200, "OK"); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java index bc6e33295..58b8fbc23 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponse.java @@ -3,6 +3,7 @@ import com.koushikdutta.async.AsyncSocket; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.callback.CompletedCallback; +import com.koushikdutta.async.http.AsyncHttpResponse; import com.koushikdutta.async.http.Headers; import org.json.JSONObject; @@ -23,6 +24,10 @@ public interface AsyncHttpServerResponse extends DataSink, CompletedCallback { public void writeHead(); public void setContentType(String contentType); public void redirect(String location); + + // NOT FINAL + public void proxy(AsyncHttpResponse response); + /** * Alias for end. Used with CompletedEmitters */ diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java index 41c20cecd..639ac77d8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java @@ -9,6 +9,7 @@ import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; import com.koushikdutta.async.callback.CompletedCallback; +import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.callback.WritableCallback; import com.koushikdutta.async.http.AsyncHttpHead; import com.koushikdutta.async.http.AsyncHttpResponse; @@ -145,7 +146,8 @@ public WritableCallback getWriteableCallback() { @Override public void end() { - if ("Chunked".equalsIgnoreCase(mRawHeaders.get("Transfer-Encoding"))) { + if ("Chunked".equalsIgnoreCase(mRawHeaders.get("Transfer-Encoding")) && mSink == null + || mSink instanceof ChunkedOutputFilter) { initFirstWrite(); ((ChunkedOutputFilter)mSink).setMaxBuffer(Integer.MAX_VALUE); mSink.write(new ByteBufferList()); @@ -291,6 +293,25 @@ public void sendFile(File file) { } } + @Override + public void proxy(final AsyncHttpResponse remoteResponse) { + code(remoteResponse.code()); + remoteResponse.headers().removeAll("Transfer-Encoding"); + remoteResponse.headers().removeAll("Content-Encoding"); + remoteResponse.headers().removeAll("Connection"); + getHeaders().addAll(remoteResponse.headers()); + // TODO: remove? + remoteResponse.headers().set("Connection", "close"); + Util.pump(remoteResponse, this, new CompletedCallback() { + @Override + public void onCompleted(Exception ex) { + remoteResponse.setEndCallback(new NullCompletedCallback()); + remoteResponse.setDataCallback(new DataCallback.NullDataCallback()); + end(); + } + }); + } + int code = 200; @Override public AsyncHttpServerResponse code(int code) { diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncProxyServer.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncProxyServer.java index 8a16c092a..077cead12 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncProxyServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncProxyServer.java @@ -55,16 +55,7 @@ public void onConnectCompleted(Exception ex, AsyncHttpResponse remoteResponse) { response.send(ex.getMessage()); return; } - response.code(remoteResponse.code()); - response.getHeaders().addAll(remoteResponse.headers()); - response.getHeaders().removeAll("Transfer-Encoding"); - response.getHeaders().removeAll("Content-Encoding"); - Util.pump(remoteResponse, response, new CompletedCallback() { - @Override - public void onCompleted(Exception ex) { - response.end(); - } - }); + response.proxy(remoteResponse); } }); } From 50afaa58b44cb452e19efa7e4b5bfc0288536e1b Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Fri, 8 Aug 2014 01:32:53 -0700 Subject: [PATCH 075/399] use thread name --- .../src/com/koushikdutta/async/future/FutureThread.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/future/FutureThread.java b/AndroidAsync/src/com/koushikdutta/async/future/FutureThread.java index ec089cc66..37091e6e1 100644 --- a/AndroidAsync/src/com/koushikdutta/async/future/FutureThread.java +++ b/AndroidAsync/src/com/koushikdutta/async/future/FutureThread.java @@ -19,6 +19,6 @@ public void run() { setComplete(e); } } - }).start(); + }, name).start(); } } From f780d542a448a1c0a2dd1bf1092dafc103bb616d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20Szyman=CC=81ski?= Date: Fri, 8 Aug 2014 11:49:04 +0200 Subject: [PATCH 076/399] Add api for sending ping and receiving pong on WebSocket --- .../koushikdutta/async/http/HybiParser.java | 11 +++++----- .../koushikdutta/async/http/WebSocket.java | 9 +++++++- .../async/http/WebSocketImpl.java | 22 +++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/HybiParser.java b/AndroidAsync/src/com/koushikdutta/async/http/HybiParser.java index 80844ad1e..e1b401590 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/HybiParser.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/HybiParser.java @@ -302,6 +302,10 @@ public byte[] frame(byte[] data, int offset, int length) { return frame(OP_BINARY, data, -1, offset, length); } + public byte[] pingFrame(String data) { + return frame(OP_PING, data, -1); + } + /** * Flip the opcode so to avoid the name collision with the public method * @@ -378,10 +382,6 @@ private byte[] frame(int opcode, byte [] data, int errorCode, int dataOffset, in return frame; } - public void ping(String message) { -// send(frame(message, OP_PING, -1)); - } - public void close(int code, String reason) { if (mClosed) return; sendFrame(frame(OP_CLOSE, reason, code)); @@ -444,13 +444,14 @@ private void emitFrame() throws IOException { } else if (opcode == OP_PONG) { String message = encode(payload); - // FIXME: Fire callback... + onPong(message); // Log.d(TAG, "Got pong! " + message); } } protected abstract void onMessage(byte[] payload); protected abstract void onMessage(String payload); + protected abstract void onPong(String payload); protected abstract void onDisconnect(int code, String reason); protected abstract void report(Exception ex); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/WebSocket.java b/AndroidAsync/src/com/koushikdutta/async/http/WebSocket.java index 8242381c1..1aaafa6d2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/WebSocket.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/WebSocket.java @@ -7,14 +7,21 @@ public interface WebSocket extends AsyncSocket { static public interface StringCallback { public void onStringAvailable(String s); } + static public interface PongCallback { + public void onPongReceived(String s); + } public void send(byte[] bytes); public void send(String string); public void send(byte [] bytes, int offset, int len); + public void ping(String message); public void setStringCallback(StringCallback callback); public StringCallback getStringCallback(); - + + public void setPongCallback(PongCallback callback); + public PongCallback getPongCallback(); + public boolean isBuffering(); public AsyncSocket getSocket(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java index 8fabcb28b..e4a2787d4 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java @@ -98,6 +98,12 @@ protected void onDisconnect(int code, String reason) { protected void sendFrame(byte[] frame) { mSink.write(ByteBuffer.wrap(frame)); } + + @Override + protected void onPong(String payload) { + if (WebSocketImpl.this.mPongCallback != null) + WebSocketImpl.this.mPongCallback.onPongReceived(payload); + } }; mParser.setMasking(masking); mParser.setDeflate(deflate); @@ -228,6 +234,11 @@ public void send(String string) { mSink.write(ByteBuffer.wrap(mParser.frame(string))); } + @Override + public void ping(String string) { + mSink.write(ByteBuffer.wrap(mParser.pingFrame(string))); + } + private StringCallback mStringCallback; @Override public void setStringCallback(StringCallback callback) { @@ -245,6 +256,17 @@ public StringCallback getStringCallback() { return mStringCallback; } + private PongCallback mPongCallback; + @Override + public void setPongCallback(PongCallback callback) { + mPongCallback = callback; + } + + @Override + public PongCallback getPongCallback() { + return mPongCallback; + } + @Override public DataCallback getDataCallback() { return mDataCallback; From 3274c82a338ff06859b68bd3dc580b4b9c3046f8 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sat, 16 Aug 2014 16:19:20 -0700 Subject: [PATCH 077/399] AsyncHttpServer: watch for socket close --- .../async/http/server/AsyncHttpServerRequestImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java index 15bed1025..6e914a20d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerRequestImpl.java @@ -93,6 +93,7 @@ void setSocket(AsyncSocket socket) { LineEmitter liner = new LineEmitter(); mSocket.setDataCallback(liner); liner.setLineCallback(mHeaderCallback); + mSocket.setEndCallback(new NullCompletedCallback()); } @Override From 1a491bc951f01f98dd41a194fcf9fac69e82ecea Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sat, 16 Aug 2014 19:07:53 -0700 Subject: [PATCH 078/399] DataEmitter.charset fixes: FilteredDataEmitter.charset now calls into underlying DataEmitter. UrlEncodedFormBody encodes as utf8. --- .../src/com/koushikdutta/async/FilteredDataEmitter.java | 7 +++++++ .../koushikdutta/async/http/body/UrlEncodedFormBody.java | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java b/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java index 6c59def72..576b5d60d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java +++ b/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java @@ -85,4 +85,11 @@ public AsyncServer getServer() { public void close() { mEmitter.close(); } + + @Override + public String charset() { + if (mEmitter == null) + return null; + return mEmitter.charset(); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java b/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java index b52fc75f2..a3fad5459 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java @@ -8,6 +8,7 @@ import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.Multimap; +import com.koushikdutta.async.util.Charsets; import org.apache.http.NameValuePair; @@ -42,7 +43,7 @@ private void buildData() { b.append('='); b.append(URLEncoder.encode(pair.getValue(), "UTF-8")); } - mBodyBytes = b.toString().getBytes("ISO-8859-1"); + mBodyBytes = b.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new AssertionError(e); @@ -59,7 +60,7 @@ public void write(AsyncHttpRequest request, final DataSink response, final Compl public static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; @Override public String getContentType() { - return CONTENT_TYPE; + return CONTENT_TYPE + "; charset=utf8"; } @Override From 00d1f567597af6e141c6788da281e32962103570 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sat, 16 Aug 2014 19:07:53 -0700 Subject: [PATCH 079/399] DataEmitter.charset fixes: FilteredDataEmitter.charset now calls into underlying DataEmitter. UrlEncodedFormBody encodes as utf8. --- .../src/com/koushikdutta/async/FilteredDataEmitter.java | 7 +++++++ .../koushikdutta/async/http/body/UrlEncodedFormBody.java | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java b/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java index 71bcfa940..10e850407 100644 --- a/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java +++ b/AndroidAsync/src/com/koushikdutta/async/FilteredDataEmitter.java @@ -85,4 +85,11 @@ public AsyncServer getServer() { public void close() { mEmitter.close(); } + + @Override + public String charset() { + if (mEmitter == null) + return null; + return mEmitter.charset(); + } } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java b/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java index b50b7c72d..7bceceb89 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/body/UrlEncodedFormBody.java @@ -8,6 +8,7 @@ import com.koushikdutta.async.callback.DataCallback; import com.koushikdutta.async.http.AsyncHttpRequest; import com.koushikdutta.async.http.Multimap; +import com.koushikdutta.async.util.Charsets; import org.apache.http.NameValuePair; @@ -42,7 +43,7 @@ private void buildData() { b.append('='); b.append(URLEncoder.encode(pair.getValue(), "UTF-8")); } - mBodyBytes = b.toString().getBytes("ISO-8859-1"); + mBodyBytes = b.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { } @@ -58,7 +59,7 @@ public void write(AsyncHttpRequest request, final DataSink response, final Compl public static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; @Override public String getContentType() { - return CONTENT_TYPE; + return CONTENT_TYPE + "; charset=utf8"; } @Override From f3213fed8a1f68d8c1c8098f7fc84b318abc7a89 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sat, 16 Aug 2014 19:11:27 -0700 Subject: [PATCH 080/399] iml --- AndroidAsync/AndroidAsync-AndroidAsync.iml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index 1cdbd092c..bf5ba2cad 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -56,7 +56,25 @@ - + + + + + + + + + + + + + + + + + + + From b939829331950f98b5cddde7a7a83d6ec8d361f1 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sat, 16 Aug 2014 19:12:59 -0700 Subject: [PATCH 081/399] merge websocket fix --- AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java index 9da4e51c8..0f113bb9a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/WebSocketImpl.java @@ -235,7 +235,7 @@ public void send(String string) { @Override public void ping(String string) { - mSink.write(ByteBuffer.wrap(mParser.pingFrame(string))); + mSink.write(new ByteBufferList(ByteBuffer.wrap(mParser.pingFrame(string)))); } private StringCallback mStringCallback; From 775814837142693baa11d882a2ec963cdddc4e89 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Tue, 26 Aug 2014 01:13:10 -0700 Subject: [PATCH 082/399] Fix up leaky alloctions. --- AndroidAsync/AndroidAsync-AndroidAsync.iml | 6 +++--- .../src/com/koushikdutta/async/AsyncNetworkSocket.java | 3 +++ .../src/com/koushikdutta/async/AsyncSSLSocketWrapper.java | 7 +++---- .../async/http/cache/ResponseCacheMiddleware.java | 2 ++ .../async/http/filter/InflaterInputFilter.java | 6 ++---- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index bf5ba2cad..73665c349 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -37,22 +37,22 @@ + - + - + - diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java b/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java index 3eb5a3cd1..5b1bfe508 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java @@ -142,6 +142,9 @@ int onReadable() { pending.add(b); Util.emitAllData(this, pending); } + else { + ByteBufferList.reclaim(b); + } if (closed) { reportEndPending(null); diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 8760bdd38..f138a471d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -364,10 +364,8 @@ public void write(ByteBufferList bb) { // if the handshake is finished, don't send // 0 bytes of data, since that makes the ssl connection die. // it wraps a 0 byte package, and craps out. - if (finishedHandshake && bb.remaining() == 0) { - mWrapping = false; - return; - } + if (finishedHandshake && bb.remaining() == 0) + break; remaining = bb.remaining(); try { ByteBuffer[] arr = bb.getAllArray(); @@ -395,6 +393,7 @@ public void write(ByteBufferList bb) { } while ((remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)) && mSink.remaining() == 0); mWrapping = false; + ByteBufferList.reclaim(writeBuf); } @Override diff --git a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java index ae8417332..bf0e92c2c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/cache/ResponseCacheMiddleware.java @@ -431,10 +431,12 @@ void spewInternal() { FileInputStream din = cacheResponse.getBody(); int read = din.read(buffer.array(), buffer.arrayOffset(), buffer.capacity()); if (read == -1) { + ByteBufferList.reclaim(buffer); allowEnd = true; report(null); return; } + allocator.track(read); buffer.limit(read); pending.add(buffer); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/InflaterInputFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/InflaterInputFilter.java index 8ec87f2b2..3ae1363a8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/InflaterInputFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/InflaterInputFilter.java @@ -36,8 +36,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { int inflated = mInflater.inflate(output.array(), output.arrayOffset() + output.position(), output.remaining()); output.position(output.position() + inflated); if (!output.hasRemaining()) { - output.limit(output.position()); - output.position(0); + output.flip(); transformed.add(output); assert totalRead != 0; int newSize = output.capacity() * 2; @@ -48,8 +47,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { } ByteBufferList.reclaim(b); } - output.limit(output.position()); - output.position(0); + output.flip(); transformed.add(output); Util.emitAllData(this, transformed); From 3f9ececb5057cce5a069651edbc30d5b56a1b836 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Tue, 26 Aug 2014 01:13:10 -0700 Subject: [PATCH 083/399] Fix up leaky alloctions. --- AndroidAsync/AndroidAsync-AndroidAsync.iml | 6 +++--- .../src/com/koushikdutta/async/AsyncNetworkSocket.java | 3 +++ .../src/com/koushikdutta/async/AsyncSSLSocketWrapper.java | 7 +++---- .../koushikdutta/async/http/ResponseCacheMiddleware.java | 2 ++ .../async/http/filter/InflaterInputFilter.java | 6 ++---- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index bf5ba2cad..73665c349 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -37,22 +37,22 @@ + - + - + - diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java b/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java index 9a15cb287..19fd2c3d2 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncNetworkSocket.java @@ -174,6 +174,9 @@ int onReadable() { pending.add(b); Util.emitAllData(this, pending); } + else { + ByteBufferList.reclaim(b); + } if (closed) { reportEndPending(null); diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 36d087c51..0e8ab1999 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -388,10 +388,8 @@ public void write(ByteBufferList bb) { // if the handshake is finished, don't send // 0 bytes of data, since that makes the ssl connection die. // it wraps a 0 byte package, and craps out. - if (finishedHandshake && bb.remaining() == 0) { - mWrapping = false; - return; - } + if (finishedHandshake && bb.remaining() == 0) + break; remaining = bb.remaining(); try { ByteBuffer[] arr = bb.getAllArray(); @@ -417,6 +415,7 @@ public void write(ByteBufferList bb) { while ((remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)) && mSink.remaining() == 0); ByteBufferList.reclaim(mWriteTmp); mWrapping = false; + ByteBufferList.reclaim(writeBuf); } @Override diff --git a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java index 851d77dd8..3715fb74a 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/ResponseCacheMiddleware.java @@ -416,10 +416,12 @@ void spewInternal() { FileInputStream din = cacheResponse.getBody(); int read = din.read(buffer.array(), buffer.arrayOffset(), buffer.capacity()); if (read == -1) { + ByteBufferList.reclaim(buffer); allowEnd = true; report(null); return; } + allocator.track(read); buffer.limit(read); pending.add(buffer); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/filter/InflaterInputFilter.java b/AndroidAsync/src/com/koushikdutta/async/http/filter/InflaterInputFilter.java index 8ec87f2b2..3ae1363a8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/filter/InflaterInputFilter.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/filter/InflaterInputFilter.java @@ -36,8 +36,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { int inflated = mInflater.inflate(output.array(), output.arrayOffset() + output.position(), output.remaining()); output.position(output.position() + inflated); if (!output.hasRemaining()) { - output.limit(output.position()); - output.position(0); + output.flip(); transformed.add(output); assert totalRead != 0; int newSize = output.capacity() * 2; @@ -48,8 +47,7 @@ public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) { } ByteBufferList.reclaim(b); } - output.limit(output.position()); - output.position(0); + output.flip(); transformed.add(output); Util.emitAllData(this, transformed); From 04dfd678e8d61c99b4a8fa6592819e910c98acd1 Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Tue, 26 Aug 2014 01:21:40 -0700 Subject: [PATCH 084/399] 138 --- AndroidAsync/AndroidManifest.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AndroidAsync/AndroidManifest.xml b/AndroidAsync/AndroidManifest.xml index 6984a72e7..ae615c1c7 100644 --- a/AndroidAsync/AndroidManifest.xml +++ b/AndroidAsync/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="138" + android:versionName="1.3.8"> From 758763ac44490003d72bd5660611c45fc8483a9f Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Tue, 26 Aug 2014 01:25:19 -0700 Subject: [PATCH 085/399] fix cherry-pick --- .../src/com/koushikdutta/async/AsyncSSLSocketWrapper.java | 1 - 1 file changed, 1 deletion(-) diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 0e8ab1999..0c53f56e3 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -415,7 +415,6 @@ public void write(ByteBufferList bb) { while ((remaining != bb.remaining() || (res != null && res.getHandshakeStatus() == HandshakeStatus.NEED_WRAP)) && mSink.remaining() == 0); ByteBufferList.reclaim(mWriteTmp); mWrapping = false; - ByteBufferList.reclaim(writeBuf); } @Override From da15c726c63eddb980be38a02e31d5d6206bc92b Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Wed, 27 Aug 2014 00:10:12 -0700 Subject: [PATCH 086/399] AsyncHttpServer: fix some bugs around end invocation behavior --- AndroidAsync/AndroidAsync-AndroidAsync.iml | 2 + .../com/koushikdutta/async/AsyncServer.java | 4 ++ .../async/http/server/AsyncHttpServer.java | 10 +++++ .../server/AsyncHttpServerResponseImpl.java | 38 ++++++++++++++----- 4 files changed, 45 insertions(+), 9 deletions(-) diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index 73665c349..60c8976e2 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -75,7 +75,9 @@ + + diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java index 450dc5b60..d6f95e0c8 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java @@ -727,6 +727,10 @@ private static void runLoop(final AsyncServer server, final SelectorWrapper sele } } } + catch (CancelledKeyException e) { + // not supposed to be thrown, but apparently is... + throw new AsyncSelectorException(e); + } catch (NullPointerException e) { throw new AsyncSelectorException(e); } diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java index d5db48b80..fbbec8038 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServer.java @@ -127,6 +127,16 @@ public void onCompleted(Exception ex) { } } res = new AsyncHttpServerResponseImpl(socket, this) { + @Override + protected void report(Exception e) { + super.report(e); + if (e != null) { + socket.setDataCallback(new NullDataCallback()); + socket.setEndCallback(new NullCompletedCallback()); + socket.close(); + } + } + @Override protected void onEnd() { super.onEnd(); diff --git a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java index 639ac77d8..2e7d2ad47 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/server/AsyncHttpServerResponseImpl.java @@ -4,7 +4,6 @@ import com.koushikdutta.async.AsyncServer; import com.koushikdutta.async.AsyncSocket; -import com.koushikdutta.async.BufferedDataSink; import com.koushikdutta.async.ByteBufferList; import com.koushikdutta.async.DataSink; import com.koushikdutta.async.Util; @@ -55,7 +54,7 @@ public void write(ByteBufferList bb) { // order is important here... assert !mEnded; // do the header write... this will call onWritable, which may be reentrant - if (!mHasWritten) + if (!headWritten) initFirstWrite(); // now check to see if the list is empty. reentrancy may cause it to empty itself. @@ -70,13 +69,13 @@ public void write(ByteBufferList bb) { mSink.write(bb); } - boolean mHasWritten = false; + boolean headWritten = false; DataSink mSink; void initFirstWrite() { - if (mHasWritten) + if (headWritten) return; - mHasWritten = true; + headWritten = true; final boolean isChunked; String currentEncoding = mRawHeaders.get("Transfer-Encoding"); @@ -103,6 +102,10 @@ void initFirstWrite() { Util.writeAll(mSocket, rh.getBytes(), new CompletedCallback() { @Override public void onCompleted(Exception ex) { + if (ex != null) { + report(ex); + return; + } if (isChunked) { ChunkedOutputFilter chunked = new ChunkedOutputFilter(mSocket); chunked.setMaxBuffer(0); @@ -116,6 +119,11 @@ public void onCompleted(Exception ex) { closedCallback = null; mSink.setWriteableCallback(writable); writable = null; + if (ended) { + // the response ended while headers were written + end(); + return; + } getServer().post(new Runnable() { @Override public void run() { @@ -144,16 +152,28 @@ public WritableCallback getWriteableCallback() { return writable; } + boolean ended; @Override public void end() { - if ("Chunked".equalsIgnoreCase(mRawHeaders.get("Transfer-Encoding")) && mSink == null - || mSink instanceof ChunkedOutputFilter) { - initFirstWrite(); + if (ended) + return; + ended = true; + if (headWritten && mSink == null) { + // header is in the process of being written... bail out. + // end will be called again after finished. + return; + } + if (!headWritten) { + // end was called, and no head or body was yet written, + // so strip the transfer encoding as that is superfluous. + mRawHeaders.remove("Transfer-Encoding"); + } + if (mSink instanceof ChunkedOutputFilter) { ((ChunkedOutputFilter)mSink).setMaxBuffer(Integer.MAX_VALUE); mSink.write(new ByteBufferList()); onEnd(); } - else if (!mHasWritten) { + else if (!headWritten) { if (!mRequest.getMethod().equalsIgnoreCase(AsyncHttpHead.METHOD)) send("text/html", ""); else { From edb529cd56c6f1f66ddf2ebcc807ba30a592b3ea Mon Sep 17 00:00:00 2001 From: Tae Hwan Kim Date: Mon, 1 Sep 2014 09:27:39 +0900 Subject: [PATCH 087/399] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c86ae658..042c0917e 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ AsyncHttpClient.getDefaultInstance().websocket(get, "my-protocol", new WebSocket ``` -### AndroidAsync also supports socket.io +### AndroidAsync also supports socket.io (version 0.9.x) ```java SocketIOClient.connect(AsyncHttpClient.getDefaultInstance(), "http://192.168.1.2:3000", new ConnectCallback() { From 6fc4bc82f4504770ae23f6be9a3894b823057b3a Mon Sep 17 00:00:00 2001 From: Koushik Dutta Date: Sun, 7 Sep 2014 16:48:16 -0700 Subject: [PATCH 088/399] AsyncHttpClient: Make SpdyMiddleware the default SSL Socket provider. Spdy disabled by default. --- AndroidAsync/AndroidAsync-AndroidAsync.iml | 1 + .../async/http/AsyncHttpClient.java | 7 +++-- .../async/http/spdy/SpdyMiddleware.java | 28 +++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/AndroidAsync/AndroidAsync-AndroidAsync.iml b/AndroidAsync/AndroidAsync-AndroidAsync.iml index 60c8976e2..e506543a0 100644 --- a/AndroidAsync/AndroidAsync-AndroidAsync.iml +++ b/AndroidAsync/AndroidAsync-AndroidAsync.iml @@ -13,6 +13,7 @@