diff options
author | Koushik Dutta <koushd@gmail.com> | 2014-07-23 01:06:25 -0700 |
---|---|---|
committer | Koushik Dutta <koushd@gmail.com> | 2014-07-23 01:06:25 -0700 |
commit | 1b08bd6375fca37e5bd41169c2e80b55f0c1f0b3 (patch) | |
tree | 9e3f10e5bd69f0c63ae2a241079430e88b21abb6 /AndroidAsync/src/com/koushikdutta/async | |
parent | 11b629da6c967fe6b3c588c0d77dbc0fdc53b4c5 (diff) | |
download | AndroidAsync-1b08bd6375fca37e5bd41169c2e80b55f0c1f0b3.tar.gz AndroidAsync-1b08bd6375fca37e5bd41169c2e80b55f0c1f0b3.tar.bz2 AndroidAsync-1b08bd6375fca37e5bd41169c2e80b55f0c1f0b3.zip |
All libcore usage hidden. Fix obscure buffering/replay bugs in SSL.
Diffstat (limited to 'AndroidAsync/src/com/koushikdutta/async')
33 files changed, 695 insertions, 376 deletions
diff --git a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java index 7f60999..84d7da1 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java @@ -115,6 +115,10 @@ public class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket } } + boolean mEnded; + Exception mEndException; + final ByteBufferList transformed = new ByteBufferList(); + private AsyncSSLSocketWrapper(AsyncSocket socket, String host, int port, SSLEngine sslEngine, @@ -140,10 +144,20 @@ public class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket // 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 @@ public class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket } } - Util.emitAllData(AsyncSSLSocketWrapper.this, transformed); + AsyncSSLSocketWrapper.this.onDataAvailable(); } catch (SSLException ex) { ex.printStackTrace(); @@ -208,6 +222,14 @@ public class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket }); } + 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 class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket 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 class AsyncSSLSocketWrapper implements AsyncSocketWrapper, AsyncSSLSocket @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 a1c7a47..b399362 100644 --- a/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java +++ b/AndroidAsync/src/com/koushikdutta/async/AsyncServer.java @@ -126,7 +126,12 @@ public class AsyncServer { 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 90e05c7..1c05617 100644 --- a/AndroidAsync/src/com/koushikdutta/async/DataEmitterBase.java +++ b/AndroidAsync/src/com/koushikdutta/async/DataEmitterBase.java @@ -8,9 +8,6 @@ import com.koushikdutta.async.callback.DataCallback; */ 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 71bcfa9..6c59def 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.callback.DataCallback; 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 class FilteredDataEmitter extends DataEmitterBase implements DataEmitter, 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 18c3736..c8df5b7 100644 --- a/AndroidAsync/src/com/koushikdutta/async/Util.java +++ b/AndroidAsync/src/com/koushikdutta/async/Util.java @@ -131,7 +131,6 @@ public class Util { 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 b6654a9..313a9fa 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.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.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 @@ public class AsyncHttpClient { 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 class AsyncHttpClient { 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 class AsyncHttpClient { 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 class AsyncHttpClient { 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 @@ public class AsyncHttpClient { 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 2404482..c706e53 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.DataEmitter; 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 interface AsyncHttpClientMiddleware { 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 fa50c20..95dd21d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpRequest.java @@ -5,7 +5,6 @@ import android.util.Log; 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 class AsyncHttpRequest { public String getMethod() { return mMethod; } - + @Override public String toString() { String path = AsyncHttpRequest.this.getUri().getEncodedPath(); @@ -88,7 +87,6 @@ public class AsyncHttpRequest { 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 class AsyncHttpRequest { 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 class AsyncHttpRequest { 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 class AsyncHttpRequest { 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 class AsyncHttpRequest { @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 class AsyncHttpRequest { @Override public Header[] getHeaders(String name) { - Map<String, List<String>> map = request.getHeaders().toMultimap(); + Map<String, List<String>> map = request.getHeaders().getMultiMap(); List<String> vals = map.get(name); if (vals == null) return new Header[0]; @@ -270,12 +257,12 @@ public class AsyncHttpRequest { @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 class AsyncHttpRequest { 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 f44ba8e..e771395 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncHttpResponse.java @@ -3,15 +3,15 @@ package com.koushikdutta.async.http; 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 e1b2399..fca0473 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.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 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 @@ abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements Asyn } }); - 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 @@ abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements Asyn onRequestCompleted(ex); } }); - } - else { + } else { onRequestCompleted(null); } } @@ -102,17 +103,25 @@ abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements Asyn 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 @@ abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements Asyn 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 @@ abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements Asyn private AsyncHttpRequest mRequest; private AsyncSocket mSocket; - protected RawHeaders mHeaders; + protected Headers mHeaders; public AsyncHttpResponseImpl(AsyncHttpRequest request) { mRequest = request; } @@ -166,23 +175,51 @@ abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements Asyn 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 @@ abstract class AsyncHttpResponseImpl extends FilteredDataEmitter implements Asyn @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 4ec7d80..59fa0f7 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.LineEmitter; 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 @@ public class AsyncSSLSocketMiddleware extends AsyncSocketMiddleware { } @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 6e7d692..37ffe5c 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/AsyncSocketMiddleware.java @@ -52,7 +52,7 @@ public class AsyncSocketMiddleware extends SimpleMiddleware { 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 class AsyncSocketMiddleware extends SimpleMiddleware { 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 class AsyncSocketMiddleware extends SimpleMiddleware { 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 class AsyncSocketMiddleware extends SimpleMiddleware { data.connectCallback.onConnectCompleted(ex, socket); } } - }, uri, port, false)); + })); } }); } @@ -360,7 +358,8 @@ public class AsyncSocketMiddleware extends SimpleMiddleware { 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 8a2146b..8fb7025 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<String, List<String>> mm) { + map.putAll(mm); + } + Multimap map = new Multimap(); public Multimap getMultiMap() { return map; @@ -29,33 +46,93 @@ public class Headers { return this; } - public List<String> 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<String> values) { + for (String v: values) { + add(header, v); + } + return this; } - public void setResponseCode(int responseCode) { - this.responseCode = responseCode; + + public Headers addAll(Map<String, List<String>> 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<String> removeAll(String header) { + return map.remove(header); + } + + public String remove(String header) { + List<String> 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<Header> ret = new ArrayList<Header>(); + 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 bbcdf8f..b7178ee 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.JSONObjectBody; 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 class HttpUtil { } } - 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 @@ public class HttpUtil { 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 @@ public class HttpUtil { 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 5223fc8..913532e 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/Multimap.java @@ -2,14 +2,11 @@ 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; 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 class Multimap extends LinkedHashMap<String, List<String>> implements Ite put(name, ret); } - public Multimap(RawHeaders headers) { - headers.toMultimap().putAll(this); - } - public Multimap(List<NameValuePair> 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 0000000..8e5a46c --- /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 <a + * href="http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04">NPN</a> and + * <a href="http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg">ALPN</a> + * selection. + * <p/> + * <h3>Protocol vs Scheme</h3> + * 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 <i>protocol</i> + * 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. + * <p/> + * <p>This version of OkHttp implements <a + * href="http://www.ietf.org/rfc/rfc2616.txt">RFC 2616</a>, 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. + * <p/> + * <p>This version of OkHttp implements SPDY 3 <a + * href="http://dev.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1">draft + * 3.1</a>. 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. + * <p/> + * <p>This version of OkHttp implements HTTP/2 <a + * href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-13">draft 12</a> + * with HPACK <a + * href="http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-08">draft + * 6</a>. 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<String, Protocol> protocols = new Hashtable<String, Protocol>(); + + 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 dba645a..7fe545d 100644 --- a/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java +++ b/AndroidAsync/src/com/koushikdutta/async/http/SimpleMiddleware.java @@ -3,14 +3,13 @@ package com.koushikdutta.async.http; 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 4af0b99..ddbd62c 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.Util; 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 class WebSocketImpl implements WebSocket { 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 class WebSocketImpl implements WebSocket { } 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 class WebSocketImpl implements WebSocket { 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 1872e19..309d9c3 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.ContinuationCallback; 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 @@ import java.util.UUID; public class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpRequestBody<Multimap> { LineEmitter liner; - RawHeaders formData; + Headers formData; ByteBufferList last; String lastName; @@ -40,7 +40,7 @@ public class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpR return; if (formData == null) - formData = new RawHeaders(); + formData = new Headers(); formData.add(lastName, last.peekString()); @@ -62,7 +62,7 @@ public class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpR @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 class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpR 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 class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpR 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 class MultipartFormDataBody extends BoundaryEmitter implements AsyncHttpR @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 56fcecb..b66c711 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 @@ package com.koushikdutta.async.http.body; 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 @@ import java.util.List; 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 class Part { private long length = -1; public Part(String name, long length, List<NameValuePair> 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 class Part { } } 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 caadf8f..0000000 --- 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<String> varyFields(RawHeaders headers) { - HashSet<String> ret = new HashSet<String>(); - 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 53be340..9274a54 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 @@ import java.util.TreeMap; * <p>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<String> FIELD_NAME_COMPARATOR = new Comparator<String>() { @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 22f0ac2..447c81b 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.AsyncHttpClient; 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 class ResponseCacheMiddleware extends SimpleMiddleware { // 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 class ResponseCacheMiddleware extends SimpleMiddleware { } // 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 class ResponseCacheMiddleware extends SimpleMiddleware { 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 class ResponseCacheMiddleware extends SimpleMiddleware { } 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 class ResponseCacheMiddleware extends SimpleMiddleware { 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 class ResponseCacheMiddleware extends SimpleMiddleware { } 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 68db69f..0000000 --- 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 d1b05a2..41aa217 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 class ContentLengthFilter extends FilteredDataEmitter { @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 1c981b7..3d65669 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.callback.ListenCallback; 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 class AsyncHttpServer { } } - 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 @@ public class AsyncHttpServer { @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 class AsyncHttpServer { 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 class AsyncHttpServer { } // 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 class AsyncHttpServer { Matcher m = p.regex.matcher(path); if (m.matches()) { mMatcher = m; - match = p; + match = p.callback; break; } } @@ -130,26 +138,26 @@ public class AsyncHttpServer { } }; - 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 class AsyncHttpServer { 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 @@ public class AsyncHttpServer { HttpServerRequestCallback callback; } - Hashtable<String, ArrayList<Pair>> mActions = new Hashtable<String, ArrayList<Pair>>(); + final Hashtable<String, ArrayList<Pair>> mActions = new Hashtable<String, ArrayList<Pair>>(); public void addAction(String action, String regex, HttpServerRequestCallback callback) { Pair p = new Pair(); @@ -284,7 +291,7 @@ public class AsyncHttpServer { } 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 class AsyncHttpServer { } } 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 class AsyncHttpServer { public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { String path = request.getMatcher().replaceAll(""); android.util.Pair<Integer, InputStream> 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 class AsyncHttpServer { public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) { String path = request.getMatcher().replaceAll(""); android.util.Pair<Integer, InputStream> 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 class AsyncHttpServer { 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 class AsyncHttpServer { } }); } - 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 77ce875..1b0292f 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 @@ package com.koushikdutta.async.http.server; 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 ba0576f..15bed10 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; 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 abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter imp 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 @@ public abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter imp @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 @@ public abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter imp 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 abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter imp } @Override - public RawHeaders getHeaders() { + public Headers getHeaders() { return mRawHeaders; } @@ -140,4 +145,11 @@ public abstract class AsyncHttpServerRequestImpl extends FilteredDataEmitter imp 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 87fbc5e..bc6e332 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 @@ package com.koushikdutta.async.http.server; 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 0fe4e10..3cc5d63 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.Util; 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 org.json.JSONObject; 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 class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { 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 @@ public class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { 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 class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { 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 class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { @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 @@ public class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { @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 class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { 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 class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { 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 class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { 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 class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { }); } catch (Exception e) { - responseCode(404); + code(500); end(); } } @@ -266,21 +265,27 @@ public class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { 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 class AsyncHttpServerResponseImpl implements AsyncHttpServerResponse { 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 0000000..8a16c09 --- /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 484b6d4..18e856c 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.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.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.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; diff --git a/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java b/AndroidAsync/src/com/koushikdutta/async/http/spdy/SpdyMiddleware.java index 837a126..35d4257 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.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.http.Protocol; import com.koushikdutta.async.util.Charsets; import java.lang.reflect.Field; |