From 60f55066cee7b11f902ca4a1259ff0021649411c Mon Sep 17 00:00:00 2001 From: Keith Wait Date: Sat, 11 Aug 2018 21:48:30 -0500 Subject: [PATCH] add file responses --- src/android/NanoHTTPDWebserver.java | 106 +++++++++++++++++++++++++++- src/android/Webserver.java | 9 +++ src/www/webserver.js | 16 +++++ webserver.js | 6 ++ 4 files changed, 136 insertions(+), 1 deletion(-) diff --git a/src/android/NanoHTTPDWebserver.java b/src/android/NanoHTTPDWebserver.java index b74ce22..858a40e 100644 --- a/src/android/NanoHTTPDWebserver.java +++ b/src/android/NanoHTTPDWebserver.java @@ -81,6 +81,105 @@ public class NanoHTTPDWebserver extends NanoHTTPD { } } + 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(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 = Response.newFixedLengthResponse(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(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(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(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 = getForbiddenResponse("Reading file failed."); + } + + return res; + } + @Override public Response serve(IHTTPSession session) { Log.d(this.getClass().getName(), "New request is incoming!"); @@ -97,7 +196,7 @@ public class NanoHTTPDWebserver extends NanoHTTPD { pluginResult.setKeepCallback(true); this.webserver.onRequestCallbackContext.sendPluginResult(pluginResult); - while (!this.webserver.responses.containsKey(requestUUID)) { + while (!this.webserver.responses.containsKey(requestUUID) && !this.webserver.responses.containsKey('file')) { try { Thread.sleep(1); } catch (InterruptedException e) { @@ -105,6 +204,11 @@ public class NanoHTTPDWebserver extends NanoHTTPD { } } + if (this.webserver.responses.containsKey('file')) { + // TODO should specify a more correct mime-type + return serveFile(session.getHeaders(), new File((String) this.webserver.responses.get('file')), 'application/octet-stream'); + } + JSONObject responseObject = (JSONObject) this.webserver.responses.get(requestUUID); Log.d(this.getClass().getName(), "responseObject: " + responseObject.toString()); Response response = null; diff --git a/src/android/Webserver.java b/src/android/Webserver.java index cf82547..812825a 100644 --- a/src/android/Webserver.java +++ b/src/android/Webserver.java @@ -44,6 +44,10 @@ public class Webserver extends CordovaPlugin { this.sendResponse(args, callbackContext); return true; } + else if ("sendFileResponse".equals(action)) { + this.sendFileResponse(args, callbackContext); + return true; + } return false; // Returning false results in a "MethodNotFound" error. } @@ -96,6 +100,11 @@ public class Webserver extends CordovaPlugin { callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); } + private void sendFileResponse(JSONArray args, CallbackContext callbackContext) throws JSONException { + this.responses.put('file', args.get(1)); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); + } + /** * Just register the onRequest and send no result. This is needed to save the callbackContext to * invoke it later diff --git a/src/www/webserver.js b/src/www/webserver.js index a1d2332..732e20c 100644 --- a/src/www/webserver.js +++ b/src/www/webserver.js @@ -4,6 +4,7 @@ const WEBSERVER_CLASS = 'Webserver'; const START_FUNCTION = 'start'; const ONREQUEST_FUNCTION = 'onRequest'; const SENDRESPONSE_FUNCION = 'sendResponse'; +const SENDFILE_FUNCION = 'sendFileResponse'; const STOP_FUNCTION = 'stop'; export function start(success_callback, error_callback, port) { @@ -45,6 +46,21 @@ export function sendResponse( ); } +export function sendFile( + requestId, + params, + success_callback, + error_callback +) { + exec( + success_callback, + error_callback, + WEBSERVER_CLASS, + SENDFILE_FUNCION, + [requestId, params] + ); +} + export function stop(success_callback, error_callback) { exec( success_callback, diff --git a/webserver.js b/webserver.js index 9ac93d6..720ac86 100644 --- a/webserver.js +++ b/webserver.js @@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { exports.start = start; exports.onRequest = onRequest; exports.sendResponse = sendResponse; +exports.sendFile = sendFile; exports.stop = stop; var _exec = require('cordova/exec'); @@ -18,6 +19,7 @@ var WEBSERVER_CLASS = 'Webserver'; var START_FUNCTION = 'start'; var ONREQUEST_FUNCTION = 'onRequest'; var SENDRESPONSE_FUNCION = 'sendResponse'; +var SENDFILE_FUNCION = 'sendFileResponse'; var STOP_FUNCTION = 'stop'; function start(success_callback, error_callback, port) { @@ -38,6 +40,10 @@ function sendResponse(requestId, params, success_callback, error_callback) { (0, _exec2.default)(success_callback, error_callback, WEBSERVER_CLASS, SENDRESPONSE_FUNCION, [requestId, params]); } +function sendFile(requestId, params, success_callback, error_callback) { + (0, _exec2.default)(success_callback, error_callback, WEBSERVER_CLASS, SENDFILE_FUNCION, [requestId, params]); +} + function stop(success_callback, error_callback) { (0, _exec2.default)(success_callback, error_callback, WEBSERVER_CLASS, STOP_FUNCTION, []); }