package org.apache.cordova.plugin; import android.net.Uri; import android.util.Log; import android.webkit.MimeTypeMap; import org.apache.cordova.PluginResult; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.UUID; import fi.iki.elonen.NanoHTTPD; public class NanoHTTPDWebserver extends NanoHTTPD { Webserver webserver; public NanoHTTPDWebserver(int port, Webserver webserver) { super(port); this.webserver = webserver; } private String getBodyText(IHTTPSession session) { Map files = new HashMap(); Method method = session.getMethod(); if (Method.PUT.equals(method) || Method.POST.equals(method)) { try { session.parseBody(files); } catch (IOException ioe) { return "{}"; } catch (ResponseException re) { return "{}"; } } // get the POST body return files.get("postData"); } /** * Create a request object *

* [ * "requestId": requestUUID, * " body": request.jsonObject ?? "", * " headers": request.headers, * " method": request.method, * " path": request.url.path, * " query": request.url.query ?? "" * ] * * @param session * @return */ private JSONObject createJSONRequest(String requestId, IHTTPSession session) throws JSONException { JSONObject jsonRequest = new JSONObject(); jsonRequest.put("requestId", requestId); jsonRequest.put("body", this.getBodyText(session)); jsonRequest.put("headers", session.getHeaders()); jsonRequest.put("method", session.getMethod()); jsonRequest.put("path", session.getUri()); jsonRequest.put("query", session.getQueryParameterString()); return jsonRequest; } private String getContentType(JSONObject responseObject) throws JSONException { if (responseObject.has("headers") && responseObject.getJSONObject("headers").has("Content-Type")) { return responseObject.getJSONObject("headers").getString("Content-Type"); } else { return "text/plain"; } } private Response newFixedFileResponse(File file, String mime) throws FileNotFoundException { Response res; res = newFixedLengthResponse(Response.Status.OK, mime, new FileInputStream(file), (int) file.length()); res.addHeader("Accept-Ranges", "bytes"); return res; } Response serveFile(Map header, File file, String mime) { Response res; try { // Calculate etag String etag = Integer.toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length()).hashCode()); // Support (simple) skipping: long startFrom = 0; long endAt = -1; String range = header.get("range"); if (range != null) { if (range.startsWith("bytes=")) { range = range.substring("bytes=".length()); int minus = range.indexOf('-'); try { if (minus > 0) { startFrom = Long.parseLong(range.substring(0, minus)); endAt = Long.parseLong(range.substring(minus + 1)); } } catch (NumberFormatException ignored) { } } } // get if-range header. If present, it must match etag or else we // should ignore the range request String ifRange = header.get("if-range"); boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange)); String ifNoneMatch = header.get("if-none-match"); boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag)); // Change return code and add Content-Range header when skipping is // requested long fileLen = file.length(); if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) { // range request that matches current etag // and the startFrom of the range is satisfiable if (headerIfNoneMatchPresentAndMatching) { // range request that matches current etag // and the startFrom of the range is satisfiable // would return range from file // respond with not-modified res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else { if (endAt < 0) { endAt = fileLen - 1; } long newLen = endAt - startFrom + 1; if (newLen < 0) { newLen = 0; } FileInputStream fis = new FileInputStream(file); fis.skip(startFrom); res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, fis, newLen); res.addHeader("Accept-Ranges", "bytes"); res.addHeader("Content-Length", "" + newLen); res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); res.addHeader("ETag", etag); } } else { if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) { // return the size of the file // 4xx responses are not trumped by if-none-match res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, ""); res.addHeader("Content-Range", "bytes */" + fileLen); res.addHeader("ETag", etag); } else if (range == null && headerIfNoneMatchPresentAndMatching) { // full-file-fetch request // would return entire file // respond with not-modified res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) { // range request that doesn't match current etag // would return entire (different) file // respond with not-modified res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); res.addHeader("ETag", etag); } else { // supply the file res = newFixedFileResponse(file, mime); res.addHeader("Content-Length", "" + fileLen); res.addHeader("ETag", etag); } } } catch (IOException ioe) { res = newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, ioe.getMessage()); } return res; } /** * Get mime type based of file extension * @param url * @return */ public static String getMimeType(String url) { String type = null; String extension = MimeTypeMap.getFileExtensionFromUrl(url); if (extension != null) { type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); } return type; } @Override public Response serve(IHTTPSession session) { Log.d(this.getClass().getName(), "New request is incoming!"); String requestUUID = UUID.randomUUID().toString(); PluginResult pluginResult = null; try { pluginResult = new PluginResult( PluginResult.Status.OK, this.createJSONRequest(requestUUID, session)); } catch (JSONException e) { e.printStackTrace(); } pluginResult.setKeepCallback(true); this.webserver.onRequestCallbackContext.sendPluginResult(pluginResult); while (!this.webserver.responses.containsKey(requestUUID)) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } JSONObject responseObject = (JSONObject) this.webserver.responses.get(requestUUID); Response response = null; Log.d(this.getClass().getName(), "responseObject: " + responseObject.toString()); if (responseObject.has("path")) { try { File file = new File(responseObject.getString("path")); Uri uri = Uri.fromFile(file); String mime = getMimeType(uri.toString()); return serveFile(session.getHeaders(), file, mime); } catch (JSONException e) { e.printStackTrace(); } return response; } else { try { response = newFixedLengthResponse( Response.Status.lookup(responseObject.getInt("status")), getContentType(responseObject), responseObject.getString("body") ); Iterator keys = responseObject.getJSONObject("headers").keys(); while (keys.hasNext()) { String key = (String) keys.next(); response.addHeader( key, responseObject.getJSONObject("headers").getString(key) ); } } catch (JSONException e) { e.printStackTrace(); } return response; } } }