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;
import com.koushikdutta.async.AsyncServer;
import com.koushikdutta.async.AsyncSocket;
import com.koushikdutta.async.ByteBufferList;
import com.koushikdutta.async.DataEmitter;
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;
import com.koushikdutta.async.future.SimpleFuture;
import com.koushikdutta.async.http.callback.HttpConnectCallback;
import com.koushikdutta.async.http.callback.RequestCallback;
import com.koushikdutta.async.http.spdy.SpdyMiddleware;
import com.koushikdutta.async.parser.AsyncParser;
import com.koushikdutta.async.parser.ByteBufferListParser;
import com.koushikdutta.async.parser.JSONArrayParser;
import com.koushikdutta.async.parser.JSONObjectParser;
import com.koushikdutta.async.parser.StringParser;
import com.koushikdutta.async.stream.OutputStreamDataCallback;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
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.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeoutException;
public class AsyncHttpClient {
private static AsyncHttpClient mDefaultInstance;
public static AsyncHttpClient getDefaultInstance() {
if (mDefaultInstance == null)
mDefaultInstance = new AsyncHttpClient(AsyncServer.getDefault());
return mDefaultInstance;
}
final List<AsyncHttpClientMiddleware> mMiddleware = new CopyOnWriteArrayList<>();
public Collection<AsyncHttpClientMiddleware> getMiddleware() {
return mMiddleware;
}
public void insertMiddleware(AsyncHttpClientMiddleware middleware) {
mMiddleware.add(0, middleware);
}
SpdyMiddleware sslSocketMiddleware;
AsyncSocketMiddleware socketMiddleware;
HttpTransportMiddleware httpTransportMiddleware;
AsyncServer mServer;
public AsyncHttpClient(AsyncServer server) {
mServer = server;
insertMiddleware(socketMiddleware = new AsyncSocketMiddleware(this));
insertMiddleware(sslSocketMiddleware = new SpdyMiddleware(this));
insertMiddleware(httpTransportMiddleware = new HttpTransportMiddleware());
sslSocketMiddleware.addEngineConfigurator(new SSLEngineSNIConfigurator());
}
@SuppressLint("NewApi")
private static void setupAndroidProxy(AsyncHttpRequest request) {
// using a explicit proxy?
if (request.proxyHost != null)
return;
List<Proxy> 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);
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;
}
public SpdyMiddleware getSSLSocketMiddleware() {
return sslSocketMiddleware;
}
public Future<AsyncHttpResponse> execute(final AsyncHttpRequest request, final HttpConnectCallback callback) {
FutureAsyncHttpResponse ret;
execute(request, 0, ret = new FutureAsyncHttpResponse(), callback);
return ret;
}
public Future<AsyncHttpResponse> execute(String uri, final HttpConnectCallback callback) {
return execute(new AsyncHttpGet(uri), callback);
}
private static final String LOGTAG = "AsyncHttp";
private class FutureAsyncHttpResponse extends SimpleFuture<AsyncHttpResponse> {
public AsyncSocket socket;
public Object scheduled;
public Runnable timeoutRunnable;
@Override
public boolean cancel() {
if (!super.cancel())
return false;
if (socket != null) {
socket.setDataCallback(new DataCallback.NullDataCallback());
socket.close();
}
if (scheduled != null)
mServer.removeAllCallbacks(scheduled);
return true;
}
}
private void reportConnectedCompleted(FutureAsyncHttpResponse cancel, Exception ex, AsyncHttpResponseImpl response, AsyncHttpRequest request, final HttpConnectCallback callback) {
assert callback != null;
mServer.removeAllCallbacks(cancel.scheduled);
boolean complete;
if (ex != null) {
request.loge("Connection error", ex);
complete = cancel.setComplete(ex);
}
else {
request.logd("Connection successful");
complete = cancel.setComplete(response);
}
if (complete) {
callback.onConnectCompleted(ex, response);
assert ex != null || response.socket() == null || response.getDataCallback() != null || response.isPaused();
return;
}
if (response != null) {
// the request was cancelled, so close up shop, and eat any pending data
response.setDataCallback(new DataCallback.NullDataCallback());
response.close();
}
}
private void execute(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) {
if (mServer.isAffinityThread()) {
executeAffinity(request, redirectCount, cancel, callback);
}
else {
mServer.post(new Runnable() {
@Override
public void run() {
executeAffinity(request, redirectCount, cancel, callback);
}
});
}
}
private static long getTimeoutRemaining(AsyncHttpRequest request) {
// need a better way to calculate this.
// a timer of sorts that stops/resumes.
return request.getTimeout();
}
private static void copyHeader(AsyncHttpRequest from, AsyncHttpRequest to, String header) {
String value = from.getHeaders().get(header);
if (!TextUtils.isEmpty(value))
to.getHeaders().set(header, value);
}
private void executeAffinity(final AsyncHttpRequest request, final int redirectCount, final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback) {
assert mServer.isAffinityThread();
if (redirectCount > 15) {
reportConnectedCompleted(cancel, new RedirectLimitExceededException("too many redirects"), null, request, callback);
return;
}
final Uri uri = request.getUri();
final AsyncHttpClientMiddleware.OnResponseCompleteDataOnRequestSentData data = new AsyncHttpClientMiddleware.OnResponseCompleteDataOnRequestSentData();
request.executionTime = System.currentTimeMillis();
data.request = request;
request.logd("Executing request.");
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onRequest(data);
}
// flow:
// 1) set a connect timeout
// 2) wait for connect
// 3) on connect, cancel timeout
// 4) wait for request to be sent fully
// 5) after request is sent, set a header timeout
// 6) wait for headers
// 7) on headers, cancel timeout
// 8) TODO: response can take as long as it wants to arrive?
if (request.getTimeout() > 0) {
// set connect timeout
cancel.timeoutRunnable = new Runnable() {
@Override
public void run() {
// we've timed out, kill the connections
if (data.socketCancellable != null) {
data.socketCancellable.cancel();
if (data.socket != null)
data.socket.close();
}
reportConnectedCompleted(cancel, new TimeoutException(), null, request, callback);
}
};
cancel.scheduled = mServer.postDelayed(cancel.timeoutRunnable, getTimeoutRemaining(request));
}
// 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)
socket.close();
return;
}
// 3) on connect, cancel timeout
if (cancel.timeoutRunnable != null)
mServer.removeAllCallbacks(cancel.scheduled);
if (ex != null) {
reportConnectedCompleted(cancel, ex, null, request, callback);
return;
}
data.socket = socket;
cancel.socket = socket;
executeSocket(request, redirectCount, cancel, callback, data);
}
};
// 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());
}
final Exception unsupportedURI;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
Cancellable socketCancellable = middleware.getSocket(data);
if (socketCancellable != null) {
data.socketCancellable = socketCancellable;
cancel.setParent(socketCancellable);
return;
}
}
unsupportedURI = new IllegalArgumentException("invalid uri="+request.getUri()+" middlewares="+mMiddleware);
reportConnectedCompleted(cancel, unsupportedURI, null, request, callback);
}
private void executeSocket(final AsyncHttpRequest request, final int redirectCount,
final FutureAsyncHttpResponse cancel, final HttpConnectCallback callback,
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;
// 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));
}
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onRequestSent(data);
}
}
@Override
public void setDataEmitter(DataEmitter emitter) {
data.bodyEmitter = emitter;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onBodyDecoder(data);
}
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(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;
}
request.logv("Final (post cache response) headers:\n" + toString());
// at this point the headers are done being modified
reportConnectedCompleted(cancel, null, this, request, callback);
}
protected void onHeadersReceived() {
super.onHeadersReceived();
if (cancel.isCancelled())
return;
// 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());
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onHeadersReceived(data);
}
// 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;
for (AsyncHttpClientMiddleware middleware: mMiddleware) {
middleware.onResponseComplete(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;
}
};
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);
for (AsyncHttpClientMiddleware middleware : mMiddleware) {
if (middleware.exchangeHeaders(data))
break;
}
}
public static abstract class RequestCallbackBase<T> implements RequestCallback<T> {
@Override
public void onProgress(AsyncHttpResponse response, long downloaded, long total) {
}
@Override
public void onConnect(AsyncHttpResponse response) {
}
}
public static abstract class DownloadCallback extends RequestCallbackBase<ByteBufferList> {
}
public static abstract class StringCallback extends RequestCallbackBase<String> {
}
public static abstract class JSONObjectCallback extends RequestCallbackBase<JSONObject> {
}
public static abstract class JSONArrayCallback extends RequestCallbackBase<JSONArray> {
}
public static abstract class FileCallback extends RequestCallbackBase<File> {
}
public Future<ByteBufferList> executeByteBufferList(AsyncHttpRequest request, DownloadCallback callback) {
return execute(request, new ByteBufferListParser(), callback);
}
public Future<String> executeString(AsyncHttpRequest req, final StringCallback callback) {
return execute(req, new StringParser(), callback);
}
public Future<JSONObject> executeJSONObject(AsyncHttpRequest req, final JSONObjectCallback callback) {
return execute(req, new JSONObjectParser(), callback);
}
public Future<JSONArray> executeJSONArray(AsyncHttpRequest req, final JSONArrayCallback callback) {
return execute(req, new JSONArrayParser(), callback);
}
private <T> void invokeWithAffinity(final RequestCallback<T> callback, SimpleFuture<T> future, final AsyncHttpResponse response, final Exception e, final T result) {
boolean complete;
if (e != null)
complete = future.setComplete(e);
else
complete = future.setComplete(result);
if (!complete)
return;
if (callback != null)
callback.onCompleted(e, response, result);
}
private <T> void invoke(final RequestCallback<T> callback, final SimpleFuture<T> future, final AsyncHttpResponse response, final Exception e, final T result) {
Runnable runnable = new Runnable() {
@Override
public void run() {
invokeWithAffinity(callback, future, response, e, result);
}
};
mServer.post(runnable);
}
private void invokeProgress(final RequestCallback callback, final AsyncHttpResponse response, final long downloaded, final long total) {
if (callback != null)
callback.onProgress(response, downloaded, total);
}
private void invokeConnect(final RequestCallback callback, final AsyncHttpResponse response) {
if (callback != null)
callback.onConnect(response);
}
public Future<File> executeFile(AsyncHttpRequest req, final String filename, final FileCallback callback) {
final File file = new File(filename);
file.getParentFile().mkdirs();
final OutputStream fout;
try {
fout = new BufferedOutputStream(new FileOutputStream(file), 8192);
}
catch (FileNotFoundException e) {
SimpleFuture<File> ret = new SimpleFuture<File>();
ret.setComplete(e);
return ret;
}
final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse();
final SimpleFuture<File> ret = new SimpleFuture<File>() {
@Override
public void cancelCleanup() {
try {
cancel.get().setDataCallback(new DataCallback.NullDataCallback());
cancel.get().close();
}
catch (Exception e) {
}
try {
fout.close();
}
catch (Exception e) {
}
file.delete();
}
};
ret.setParent(cancel);
execute(req, 0, cancel, new HttpConnectCallback() {
long mDownloaded = 0;
@Override
public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) {
if (ex != null) {
try {
fout.close();
}
catch (IOException e) {
}
file.delete();
invoke(callback, ret, response, ex, null);
return;
}
invokeConnect(callback, response);
final long contentLength = HttpUtil.contentLength(response.headers());
response.setDataCallback(new OutputStreamDataCallback(fout) {
@Override
public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
mDownloaded += bb.remaining();
super.onDataAvailable(emitter, bb);
invokeProgress(callback, response, mDownloaded, contentLength);
}
});
response.setEndCallback(new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
try {
fout.close();
}
catch (IOException e) {
ex = e;
}
if (ex != null) {
file.delete();
invoke(callback, ret, response, ex, null);
}
else {
invoke(callback, ret, response, null, file);
}
}
});
}
});
return ret;
}
public <T> SimpleFuture<T> execute(AsyncHttpRequest req, final AsyncParser<T> parser, final RequestCallback<T> callback) {
final FutureAsyncHttpResponse cancel = new FutureAsyncHttpResponse();
final SimpleFuture<T> ret = new SimpleFuture<T>();
execute(req, 0, cancel, new HttpConnectCallback() {
@Override
public void onConnectCompleted(Exception ex, final AsyncHttpResponse response) {
if (ex != null) {
invoke(callback, ret, response, ex, null);
return;
}
invokeConnect(callback, response);
Future<T> parsed = parser.parse(response)
.setCallback(new FutureCallback<T>() {
@Override
public void onCompleted(Exception e, T result) {
invoke(callback, ret, response, e, result);
}
});
// reparent to the new parser future
ret.setParent(parsed);
}
});
ret.setParent(cancel);
return ret;
}
public static interface WebSocketConnectCallback {
public void onCompleted(Exception ex, WebSocket webSocket);
}
public Future<WebSocket> websocket(final AsyncHttpRequest req, String protocol, final WebSocketConnectCallback callback) {
WebSocketImpl.addWebSocketUpgradeHeaders(req, protocol);
final SimpleFuture<WebSocket> ret = new SimpleFuture<WebSocket>();
Cancellable connect = execute(req, new HttpConnectCallback() {
@Override
public void onConnectCompleted(Exception ex, AsyncHttpResponse response) {
if (ex != null) {
if (ret.setComplete(ex)) {
if (callback != null)
callback.onCompleted(ex, null);
}
return;
}
WebSocket ws = WebSocketImpl.finishHandshake(req.getHeaders(), response);
if (ws == null) {
ex = new WebSocketHandshakeException("Unable to complete websocket handshake");
if (!ret.setComplete(ex))
return;
}
else {
if (!ret.setComplete(ws))
return;
}
if (callback != null)
callback.onCompleted(ex, ws);
}
});
ret.setParent(connect);
return ret;
}
public Future<WebSocket> websocket(String uri, String protocol, final WebSocketConnectCallback callback) {
// assert callback != null;
final AsyncHttpGet get = new AsyncHttpGet(uri.replace("ws://", "http://").replace("wss://", "https://"));
return websocket(get, protocol, callback);
}
public AsyncServer getServer() {
return mServer;
}
}