package com.koushikdutta.async.http.server;
import android.content.Context;
import com.koushikdutta.async.*;
import com.koushikdutta.async.callback.CompletedCallback;
import com.koushikdutta.async.callback.ListenCallback;
import com.koushikdutta.async.http.*;
import com.koushikdutta.async.http.libcore.RawHeaders;
import com.koushikdutta.async.http.libcore.RequestHeaders;
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class AsyncHttpServer {
ArrayList<AsyncServerSocket> mListeners = new ArrayList<AsyncServerSocket>();
public void stop() {
if (mListeners != null) {
for (AsyncServerSocket listener: mListeners) {
listener.stop();
}
}
}
protected void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
}
ListenCallback mListenCallback = new ListenCallback() {
@Override
public void onAccepted(final AsyncSocket socket) {
AsyncHttpServerRequestImpl req = new AsyncHttpServerRequestImpl() {
Pair match;
String fullPath;
String path;
boolean responseComplete;
boolean requestComplete;
AsyncHttpServerResponseImpl res;
boolean hasContinued;
@Override
protected void onHeadersReceived() {
RawHeaders headers = getRawHeaders();
// 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 = headers.getStatusLine();
String[] parts = statusLine.split(" ");
fullPath = parts[1];
path = fullPath.split("\\?")[0];
method = parts[0];
synchronized (mActions) {
ArrayList<Pair> pairs = mActions.get(method);
if (pairs != null) {
for (Pair p: pairs) {
Matcher m = p.regex.matcher(path);
if (m.matches()) {
mMatcher = m;
match = p;
break;
}
}
}
}
res = new AsyncHttpServerResponseImpl(socket, this) {
@Override
protected void onEnd() {
mSocket.setEndCallback(null);
responseComplete = true;
// reuse the socket for a subsequent request.
handleOnCompleted();
}
};
onRequest(this, res);
if (match == null) {
res.responseCode(404);
res.end();
return;
}
if (!getBody().readFullyOnRequest()) {
match.callback.onRequest(this, res);
}
else if (requestComplete) {
match.callback.onRequest(this, res);
}
}
@Override
public void onCompleted(Exception e) {
// if the protocol was switched off http, ignore this request/response.
if (res.getHeaders().getHeaders().getResponseCode() == 101)
return;
requestComplete = true;
super.onCompleted(e);
mSocket.setDataCallback(null);
mSocket.pause();
handleOnCompleted();
if (getBody().readFullyOnRequest()) {
match.callback.onRequest(this, res);
}
}
private void handleOnCompleted() {
if (requestComplete && responseComplete) {
onAccepted(socket);
}
}
@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 void listen(AsyncServer server, int port) {
server.listen(null, port, mListenCallback);
}
private void report(Exception ex) {
if (mCompletedCallback != null)
mCompletedCallback.onCompleted(ex);
}
public void listen(int port) {
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 sslSocket = new AsyncSSLSocketWrapper(socket, null, port, sslContext, null, null, false);
mListenCallback.onAccepted(sslSocket);
}
@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;
}
Hashtable<String, ArrayList<Pair>> mActions = new Hashtable<String, ArrayList<Pair>>();
public void addAction(String action, String regex, HttpServerRequestCallback callback) {
Pair p = new Pair();
p.regex = Pattern.compile("^" + regex);
p.callback = callback;
synchronized (mActions) {
ArrayList<Pair> pairs = mActions.get(action);
if (pairs == null) {
pairs = new ArrayList<AsyncHttpServer.Pair>();
mActions.put(action, pairs);
}
pairs.add(p);
}
}
public static interface WebSocketRequestCallback {
public void onConnected(WebSocket webSocket, RequestHeaders headers);
}
public void websocket(String regex, final WebSocketRequestCallback callback) {
get(regex, new HttpServerRequestCallback() {
@Override
public void onRequest(final AsyncHttpServerRequest request, final AsyncHttpServerResponse response) {
boolean hasUpgrade = false;
String connection = request.getHeaders().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().getHeaders().get("Upgrade")) || !hasUpgrade) {
response.responseCode(404);
response.end();
return;
}
callback.onConnected(new WebSocketImpl(request, response), request.getHeaders());
}
});
}
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 InputStream getAssetStream(final Context context, String asset) {
String apkPath = context.getPackageResourcePath();
String assetPath = "assets/" + asset;
try {
ZipFile zip = new ZipFile(apkPath);
Enumeration<?> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) entries.nextElement();
if (entry.getName().equals(assetPath)) {
return zip.getInputStream(entry);
}
}
}
catch (Exception ex) {
}
return null;
}
static Hashtable<String, String> mContentTypes = new Hashtable<String, String>();
{
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");
}
public static String getContentType(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 "text/plain";
}
public void directory(Context _context, String regex, final String assetPath) {
final Context context = _context.getApplicationContext();
addAction("GET", regex, new HttpServerRequestCallback() {
@Override
public void onRequest(AsyncHttpServerRequest request, final AsyncHttpServerResponse response) {
String path = request.getMatcher().replaceAll("");
InputStream is = getAssetStream(context, assetPath + path);
if (is == null) {
response.responseCode(404);
response.end();
return;
}
response.responseCode(200);
response.getHeaders().getHeaders().add("Content-Type", getContentType(path));
Util.pump(is, response, new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
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<File> dirs = new ArrayList<File>();
ArrayList<File> files = new ArrayList<File>();
for (File f: file.listFiles()) {
if (f.isDirectory())
dirs.add(f);
else
files.add(f);
}
Comparator<File> c = new Comparator<File>() {
@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.responseCode(404);
response.end();
return;
}
try {
FileInputStream is = new FileInputStream(file);
response.responseCode(200);
Util.pump(is, response, new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
response.end();
}
});
}
catch (Exception ex) {
response.responseCode(404);
response.end();
return;
}
}
});
}
private static Hashtable<Integer, String> mCodes = new Hashtable<Integer, String>();
static {
mCodes.put(200, "OK");
mCodes.put(206, "Partial Content");
mCodes.put(101, "Switching Protocols");
mCodes.put(404, "Not Found");
}
public static String getResponseCodeDescription(int code) {
String d = mCodes.get(code);
if (d == null)
return "Unknown";
return d;
}
}