com.koushikdutta.async.http.server.AsyncHttpServer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of androidasync Show documentation
Show all versions of androidasync Show documentation
Asynchronous socket, http(s) (client+server) and websocket library for android. Based on nio, not threads.
package com.koushikdutta.async.http.server;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Build;
import android.text.TextUtils;
import com.koushikdutta.async.AsyncSSLSocket;
import com.koushikdutta.async.AsyncSSLSocketWrapper;
import com.koushikdutta.async.AsyncServer;
import com.koushikdutta.async.AsyncServerSocket;
import com.koushikdutta.async.AsyncSocket;
import com.koushikdutta.async.ByteBufferList;
import com.koushikdutta.async.DataEmitter;
import com.koushikdutta.async.Util;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.callback.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.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;
import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLContext;
@TargetApi(Build.VERSION_CODES.ECLAIR)
public class AsyncHttpServer {
ArrayList mListeners = new ArrayList();
public void stop() {
if (mListeners != null) {
for (AsyncServerSocket listener: mListeners) {
listener.stop();
}
}
}
protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
return false;
}
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"));
}
ListenCallback mListenCallback = new ListenCallback() {
@Override
public void onAccepted(final AsyncSocket socket) {
AsyncHttpServerRequestImpl req = new AsyncHttpServerRequestImpl() {
HttpServerRequestCallback match;
String fullPath;
String path;
boolean responseComplete;
boolean requestComplete;
AsyncHttpServerResponseImpl res;
boolean hasContinued;
@Override
protected AsyncHttpRequestBody onUnknownBody(Headers headers) {
return AsyncHttpServer.this.onUnknownBody(headers);
}
@Override
protected void onHeadersReceived() {
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.
if (!hasContinued && "100-continue".equals(headers.get("Expect"))) {
pause();
// System.out.println("continuing...");
Util.writeAll(mSocket, "HTTP/1.1 100 Continue\r\n\r\n".getBytes(), new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
resume();
if (ex != null) {
report(ex);
return;
}
hasContinued = true;
onHeadersReceived();
}
});
return;
}
// System.out.println(headers.toHeaderString());
String statusLine = getStatusLine();
String[] parts = statusLine.split(" ");
fullPath = parts[1];
path = fullPath.split("\\?")[0];
method = parts[0];
synchronized (mActions) {
ArrayList pairs = mActions.get(method);
if (pairs != null) {
for (Pair p: pairs) {
Matcher m = p.regex.matcher(path);
if (m.matches()) {
mMatcher = m;
match = p.callback;
break;
}
}
}
}
res = new AsyncHttpServerResponseImpl(socket, this) {
@Override
protected void report(Exception e) {
super.report(e);
if (e != null) {
socket.setDataCallback(new NullDataCallback());
socket.setEndCallback(new NullCompletedCallback());
socket.close();
}
}
@Override
protected void onEnd() {
super.onEnd();
mSocket.setEndCallback(null);
responseComplete = true;
// reuse the socket for a subsequent request.
handleOnCompleted();
}
};
boolean handled = onRequest(this, res);
if (match == null && !handled) {
res.code(404);
res.end();
return;
}
if (!getBody().readFullyOnRequest()) {
onRequest(match, this, res);
}
else if (requestComplete) {
onRequest(match, this, res);
}
}
@Override
public void onCompleted(Exception e) {
// if the protocol was switched off http, ignore this request/response.
if (res.code() == 101)
return;
requestComplete = true;
super.onCompleted(e);
// no http pipelining, gc trashing if the socket dies
// while the request is being sent and is paused or something
mSocket.setDataCallback(new NullDataCallback() {
@Override
public void onDataAvailable(DataEmitter emitter, ByteBufferList bb) {
super.onDataAvailable(emitter, bb);
mSocket.close();
}
});
handleOnCompleted();
if (getBody().readFullyOnRequest()) {
onRequest(match, this, res);
}
}
private void handleOnCompleted() {
if (requestComplete && responseComplete) {
if (HttpUtil.isKeepAlive(Protocol.HTTP_1_1, getHeaders())) {
onAccepted(socket);
}
else {
socket.close();
}
}
}
@Override
public String getPath() {
return path;
}
@Override
public Multimap getQuery() {
String[] parts = fullPath.split("\\?", 2);
if (parts.length < 2)
return new Multimap();
return Multimap.parseQuery(parts[1]);
}
};
req.setSocket(socket);
socket.resume();
}
@Override
public void onCompleted(Exception error) {
report(error);
}
@Override
public void onListening(AsyncServerSocket socket) {
mListeners.add(socket);
}
};
public AsyncServerSocket listen(AsyncServer server, int port) {
return server.listen(null, port, mListenCallback);
}
private void report(Exception ex) {
if (mCompletedCallback != null)
mCompletedCallback.onCompleted(ex);
}
public AsyncServerSocket listen(int port) {
return listen(AsyncServer.getDefault(), port);
}
public void listenSecure(final int port, final SSLContext sslContext) {
AsyncServer.getDefault().listen(null, port, new ListenCallback() {
@Override
public void onAccepted(AsyncSocket socket) {
AsyncSSLSocketWrapper.handshake(socket, null, port, sslContext.createSSLEngine(), null, null, false,
new AsyncSSLSocketWrapper.HandshakeCallback() {
@Override
public void onHandshakeCompleted(Exception e, AsyncSSLSocket socket) {
if (socket != null)
mListenCallback.onAccepted(socket);
}
});
}
@Override
public void onListening(AsyncServerSocket socket) {
mListenCallback.onListening(socket);
}
@Override
public void onCompleted(Exception ex) {
mListenCallback.onCompleted(ex);
}
});
}
public ListenCallback getListenCallback() {
return mListenCallback;
}
CompletedCallback mCompletedCallback;
public void setErrorCallback(CompletedCallback callback) {
mCompletedCallback = callback;
}
public CompletedCallback getErrorCallback() {
return mCompletedCallback;
}
private static class Pair {
Pattern regex;
HttpServerRequestCallback callback;
}
final Hashtable> mActions = new Hashtable>();
public void addAction(String action, String regex, HttpServerRequestCallback callback) {
Pair p = new Pair();
p.regex = Pattern.compile("^" + regex);
p.callback = callback;
synchronized (mActions) {
ArrayList pairs = mActions.get(action);
if (pairs == null) {
pairs = new ArrayList();
mActions.put(action, pairs);
}
pairs.add(p);
}
}
public static interface WebSocketRequestCallback {
public void onConnected(WebSocket webSocket, AsyncHttpServerRequest request);
}
public void websocket(String regex, final WebSocketRequestCallback callback) {
websocket(regex, null, callback);
}
public void websocket(String regex, final String protocol, final WebSocketRequestCallback callback) {
get(regex, new HttpServerRequestCallback() {
@Override
public void onRequest(final AsyncHttpServerRequest request, final AsyncHttpServerResponse response) {
boolean hasUpgrade = false;
String connection = request.getHeaders().get("Connection");
if (connection != null) {
String[] connections = connection.split(",");
for (String c: connections) {
if ("Upgrade".equalsIgnoreCase(c.trim())) {
hasUpgrade = true;
break;
}
}
}
if (!"websocket".equalsIgnoreCase(request.getHeaders().get("Upgrade")) || !hasUpgrade) {
response.code(404);
response.end();
return;
}
String peerProtocol = request.getHeaders().get("Sec-WebSocket-Protocol");
if (!TextUtils.equals(protocol, peerProtocol)) {
response.code(404);
response.end();
return;
}
callback.onConnected(new WebSocketImpl(request, response), request);
}
});
}
public void get(String regex, HttpServerRequestCallback callback) {
addAction(AsyncHttpGet.METHOD, regex, callback);
}
public void post(String regex, HttpServerRequestCallback callback) {
addAction(AsyncHttpPost.METHOD, regex, callback);
}
public static android.util.Pair getAssetStream(final Context context, String asset) {
AssetManager am = context.getAssets();
try {
InputStream is = am.open(asset);
return new android.util.Pair(is.available(), is);
}
catch (IOException e) {
return null;
}
}
static Hashtable mContentTypes = new Hashtable();
{
mContentTypes.put("js", "application/javascript");
mContentTypes.put("json", "application/json");
mContentTypes.put("png", "image/png");
mContentTypes.put("jpg", "image/jpeg");
mContentTypes.put("html", "text/html");
mContentTypes.put("css", "text/css");
mContentTypes.put("mp4", "video/mp4");
mContentTypes.put("mov", "video/quicktime");
mContentTypes.put("wmv", "video/x-ms-wmv");
}
public static String getContentType(String path) {
String type = tryGetContentType(path);
if (type != null)
return type;
return "text/plain";
}
public static String tryGetContentType(String path) {
int index = path.lastIndexOf(".");
if (index != -1) {
String e = path.substring(index + 1);
String ct = mContentTypes.get(e);
if (ct != null)
return ct;
}
return null;
}
public void directory(Context context, String regex, final String assetPath) {
final Context _context = context.getApplicationContext();
addAction(AsyncHttpGet.METHOD, regex, new HttpServerRequestCallback() {
@Override
public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) {
String path = request.getMatcher().replaceAll("");
android.util.Pair pair = getAssetStream(_context, assetPath + path);
if (pair == null || pair.second == null) {
response.code(404);
response.end();
return;
}
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
public void onCompleted(Exception ex) {
response.end();
StreamUtility.closeQuietly(is);
}
});
}
});
addAction(AsyncHttpHead.METHOD, regex, new HttpServerRequestCallback() {
@Override
public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) {
String path = request.getMatcher().replaceAll("");
android.util.Pair pair = getAssetStream(_context, assetPath + path);
if (pair == null || pair.second == null) {
response.code(404);
response.end();
return;
}
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();
}
});
}
public void directory(String regex, final File directory) {
directory(regex, directory, false);
}
public void directory(String regex, final File directory, final boolean list) {
assert directory.isDirectory();
addAction("GET", regex, new HttpServerRequestCallback() {
@Override
public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) {
String path = request.getMatcher().replaceAll("");
File file = new File(directory, path);
if (file.isDirectory() && list) {
ArrayList dirs = new ArrayList();
ArrayList files = new ArrayList();
for (File f: file.listFiles()) {
if (f.isDirectory())
dirs.add(f);
else
files.add(f);
}
Comparator c = new Comparator() {
@Override
public int compare(File lhs, File rhs) {
return lhs.getName().compareTo(rhs.getName());
}
};
Collections.sort(dirs, c);
Collections.sort(files, c);
files.addAll(0, dirs);
return;
}
if (!file.isFile()) {
response.code(404);
response.end();
return;
}
try {
FileInputStream is = new FileInputStream(file);
response.code(200);
Util.pump(is, response, new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
response.end();
}
});
}
catch (FileNotFoundException ex) {
response.code(404);
response.end();
}
}
});
}
private static Hashtable mCodes = new Hashtable();
static {
mCodes.put(200, "OK");
mCodes.put(206, "Partial Content");
mCodes.put(101, "Switching Protocols");
mCodes.put(301, "Moved Permanently");
mCodes.put(302, "Found");
mCodes.put(404, "Not Found");
}
public static String getResponseCodeDescription(int code) {
String d = mCodes.get(code);
if (d == null)
return "Unknown";
return d;
}
}