package com.koushikdutta.async.http.server; import android.content.Context; import android.content.res.AssetManager; import android.text.TextUtils; 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.NullDataCallback; 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.HttpUtil; import com.koushikdutta.async.http.Multimap; import com.koushikdutta.async.http.WebSocket; import com.koushikdutta.async.http.WebSocketImpl; import com.koushikdutta.async.http.body.AsyncHttpRequestBody; import com.koushikdutta.async.http.libcore.IoUtils; import com.koushikdutta.async.http.libcore.RawHeaders; import com.koushikdutta.async.http.libcore.RequestHeaders; import com.koushikdutta.async.util.StreamUtility; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.Hashtable; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import javax.net.ssl.SSLContext; 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) { } protected AsyncHttpRequestBody onUnknownBody(RawHeaders headers) { return new UnknownRequestBody(headers.get("Content-Type")); } 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 AsyncHttpRequestBody onUnknownBody(RawHeaders headers) { return AsyncHttpServer.this.onUnknownBody(headers); } @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() { super.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); // 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()) { if (match != null) match.callback.onRequest(this, res); } } private void handleOnCompleted() { if (requestComplete && responseComplete) { if (HttpUtil.isKeepAlive(getHeaders().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 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) { 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().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; } String peerProtocol = request.getHeaders().getHeaders().get("Sec-WebSocket-Protocol"); if (!TextUtils.equals(protocol, peerProtocol)) { 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 android.util.Pair<Integer, InputStream> getAssetStream(final Context context, String asset) { AssetManager am = context.getAssets(); try { InputStream is = am.open(asset); return new android.util.Pair<Integer, InputStream>(is.available(), is); } catch (IOException e) { 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"); 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<Integer, InputStream> pair = getAssetStream(_context, assetPath + path); final InputStream is = pair.second; response.getHeaders().getHeaders().set("Content-Length", String.valueOf(pair.first)); if (is == null) { response.responseCode(404); response.end(); return; } response.responseCode(200); response.getHeaders().getHeaders().add("Content-Type", getContentType(assetPath + path)); Util.pump(is, response, new CompletedCallback() { @Override public void onCompleted(Exception ex) { response.end(); IoUtils.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<Integer, InputStream> pair = getAssetStream(_context, assetPath + path); final InputStream is = pair.second; IoUtils.closeQuietly(is); response.getHeaders().getHeaders().set("Content-Length", String.valueOf(pair.first)); if (is == null) { response.responseCode(404); response.end(); return; } response.responseCode(200); response.getHeaders().getHeaders().add("Content-Type", getContentType(assetPath + path)); response.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<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(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; } }