├── .babelrc ├── src ├── ios │ ├── GCDWebServer-Bridging-Header.h │ ├── SynchronizedDictionary.swift │ └── Webserver.swift ├── www │ └── webserver.js └── android │ ├── Webserver.java │ └── NanoHTTPDWebserver.java ├── tests ├── package.json ├── plugin.xml └── tests.js ├── LICENSE ├── .gitignore ├── package.json ├── webserver.js ├── plugin.xml └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/ios/GCDWebServer-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | #import 7 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-webserver-tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "cordova": { 6 | "id": "cordova-plugin-webserver-tests", 7 | "platforms": [] 8 | }, 9 | "keywords": [ 10 | "ecosystem:cordova" 11 | ], 12 | "author": "Michael Bykovski", 13 | "license": "Apache 2.0" 14 | } 15 | -------------------------------------------------------------------------------- /tests/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | Cordova Plugin Webserver Tests 7 | Apache 2.0 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ios/SynchronizedDictionary.swift: -------------------------------------------------------------------------------- 1 | public class SynchronizedDictionary { 2 | private var dictionary: [KeyType:ValueType] = [:] 3 | private let accessQueue = DispatchQueue(label: "SynchronizedDictionaryAccess", attributes: .concurrent) 4 | 5 | public func removeValue(forKey: KeyType) { 6 | self.accessQueue.async(flags:.barrier) { 7 | self.dictionary.removeValue(forKey: forKey) 8 | } 9 | } 10 | 11 | public subscript(key: KeyType) -> ValueType? { 12 | set { 13 | self.accessQueue.async(flags:.barrier) { 14 | self.dictionary[key] = newValue 15 | } 16 | } 17 | get { 18 | var element: ValueType? 19 | self.accessQueue.sync { 20 | element = self.dictionary[key] 21 | } 22 | return element 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Michael Bykovski 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Copyright for portions of project cordova-plugin-webserver are held 2012-2013 by Paul S. Hawke; 2001,2005-2013 by Jarno Elonen; 2010 by Konstantinos Togias as part of project NanoHttpd/nanohttpd and are provided under the BSD 3-Clause "New" or "Revised" License. 16 | Copyright for additional portions of project cordova-plugon-webserver are held 2012-2014 by Pierre-Olivier Latour as part of project swisspol/GCDWebServer and are provided under the BSD license. 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | webserver-test 64 | -------------------------------------------------------------------------------- /src/www/webserver.js: -------------------------------------------------------------------------------- 1 | import exec from 'cordova/exec'; 2 | 3 | const WEBSERVER_CLASS = 'Webserver'; 4 | const START_FUNCTION = 'start'; 5 | const ONREQUEST_FUNCTION = 'onRequest'; 6 | const SENDRESPONSE_FUNCION = 'sendResponse'; 7 | const STOP_FUNCTION = 'stop'; 8 | 9 | export function start(success_callback, error_callback, port) { 10 | let params = []; 11 | if (port) { 12 | params.push(port); 13 | } 14 | exec( 15 | success_callback, 16 | error_callback, 17 | WEBSERVER_CLASS, 18 | START_FUNCTION, 19 | params 20 | ); 21 | } 22 | 23 | export function onRequest(success_callback) { 24 | exec( 25 | success_callback, 26 | function(error) {console.error(error)}, 27 | WEBSERVER_CLASS, 28 | ONREQUEST_FUNCTION, 29 | [] 30 | ); 31 | } 32 | 33 | export function sendResponse( 34 | requestId, 35 | params, 36 | success_callback, 37 | error_callback 38 | ) { 39 | exec( 40 | success_callback, 41 | error_callback, 42 | WEBSERVER_CLASS, 43 | SENDRESPONSE_FUNCION, 44 | [requestId, params] 45 | ); 46 | } 47 | 48 | export function stop(success_callback, error_callback) { 49 | exec( 50 | success_callback, 51 | error_callback, 52 | WEBSERVER_CLASS, 53 | STOP_FUNCTION, 54 | [] 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-webserver", 3 | "version": "1.0.1", 4 | "description": "Cordova Plugin Webserver", 5 | "homepage": "https://github.com/bykof/cordova-plugin-webserver/", 6 | "author": { 7 | "name": "Michael Bykovski" 8 | }, 9 | "license": "Apache-2.0", 10 | "cordova": { 11 | "id": "cordova-plugin-webserver", 12 | "platforms": [ 13 | "ios", 14 | "android" 15 | ] 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/bykof/cordova-plugin-webserver" 20 | }, 21 | "keywords": [ 22 | "cordova", 23 | "ios", 24 | "android", 25 | "ecosystem:cordova", 26 | "cordova-ios", 27 | "cordova:plugin", 28 | "webserver", 29 | "http", 30 | "request", 31 | "response" 32 | ], 33 | "devDependencies": { 34 | "babel-cli": "^6.26.0", 35 | "babel-core": "^6.26.3", 36 | "babel-preset-env": "^1.7.0", 37 | "babel-preset-es2015": "^6.24.1", 38 | "babel-preset-stage-2": "^6.24.1", 39 | "cordova": "^11.0.0" 40 | }, 41 | "scripts": { 42 | "build": "./node_modules/babel-cli/bin/babel.js src/www/webserver.js --out-file webserver.js" 43 | }, 44 | "browser": { 45 | "cordova": false 46 | }, 47 | "dependencies": { 48 | "universal-router": "^9.1.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /webserver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.start = start; 7 | exports.onRequest = onRequest; 8 | exports.sendResponse = sendResponse; 9 | exports.stop = stop; 10 | 11 | var _exec = require('cordova/exec'); 12 | 13 | var _exec2 = _interopRequireDefault(_exec); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | var WEBSERVER_CLASS = 'Webserver'; 18 | var START_FUNCTION = 'start'; 19 | var ONREQUEST_FUNCTION = 'onRequest'; 20 | var SENDRESPONSE_FUNCION = 'sendResponse'; 21 | var STOP_FUNCTION = 'stop'; 22 | 23 | function start(success_callback, error_callback, port) { 24 | var params = []; 25 | if (port) { 26 | params.push(port); 27 | } 28 | (0, _exec2.default)(success_callback, error_callback, WEBSERVER_CLASS, START_FUNCTION, params); 29 | } 30 | 31 | function onRequest(success_callback) { 32 | (0, _exec2.default)(success_callback, function (error) { 33 | console.error(error); 34 | }, WEBSERVER_CLASS, ONREQUEST_FUNCTION, []); 35 | } 36 | 37 | function sendResponse(requestId, params, success_callback, error_callback) { 38 | (0, _exec2.default)(success_callback, error_callback, WEBSERVER_CLASS, SENDRESPONSE_FUNCION, [requestId, params]); 39 | } 40 | 41 | function stop(success_callback, error_callback) { 42 | (0, _exec2.default)(success_callback, error_callback, WEBSERVER_CLASS, STOP_FUNCTION, []); 43 | } 44 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | Webserver for Cordova Apps 10 | webserver,cordova,http, request, response,server 11 | https://github.com/bykof/cordova-plugin-webserver 12 | https://github.com/bykof/cordova-plugin-webserver/issues 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | function request(method, url, data, callback) { 2 | var xhttp = new XMLHttpRequest(); 3 | xhttp.onreadystatechange = function() { 4 | if (this.readyState === 4 && this.status === 200) { 5 | callback(this); 6 | } 7 | }; 8 | xhttp.open(method, url, true); 9 | xhttp.send(data); 10 | } 11 | 12 | exports.defineAutoTests = function() { 13 | 14 | describe('Webserver (window.webserver)', function () { 15 | var fns = [ 16 | 'start' 17 | ]; 18 | 19 | it('should exist', function() { 20 | expect(webserver).toBeDefined(); 21 | }); 22 | 23 | fns.forEach(function(fn) { 24 | it('should contain a ' + fn + ' function', function () { 25 | expect(typeof webserver[fn]).toBeDefined(); 26 | expect(typeof webserver[fn] === 'function').toBe(true); 27 | }); 28 | }) 29 | }); 30 | 31 | describe('Do a request', function() { 32 | 33 | it('should do a request with plaintext', function() { 34 | webserver.onRequest( 35 | function(request) { 36 | webserver.sendResponse( 37 | request.requestId, 38 | { 39 | status: 200, 40 | headers: { 41 | 'Content-Type': 'text/plain', 42 | 'TestHeader': 'Just a testheader' 43 | }, 44 | body: 'Test success!' 45 | } 46 | ) 47 | } 48 | ); 49 | websever.start(); 50 | 51 | request('GET', 'localhost:8080', undefined, function (response) { 52 | expect(response.responseText).toBe('Test success!'); 53 | }); 54 | }); 55 | }); 56 | }; 57 | 58 | exports.defineManualTests = function(contentEl, createActionButton) { 59 | createActionButton('Start bljad Webserver', function() { 60 | console.log("Starting webserver..."); 61 | 62 | webserver.onRequest( 63 | function(request) { 64 | console.log('Received request'); 65 | console.log('requestId: ', request.requestId); 66 | console.log('body: ', request.body); 67 | console.log('headers: ', request.headers); 68 | console.log('path: ', request.path); 69 | console.log('query: ', request.query); 70 | console.log('method: ', request.method); 71 | 72 | webserver.sendResponse( 73 | request.requestId, 74 | { 75 | status: 200, 76 | headers: { 77 | 'Content-Type': 'text/html', 78 | 'TestHeader': 'Just a testheader' 79 | }, 80 | body: '
' 81 | } 82 | ); 83 | } 84 | ); 85 | 86 | webserver.start( 87 | function() { 88 | console.log('Success!'); 89 | }, 90 | function() { 91 | console.log('Error!'); 92 | } 93 | ); 94 | }); 95 | 96 | createActionButton('Start Webserver with Port 1337', function() { 97 | console.log("Starting webserver..."); 98 | 99 | webserver.start( 100 | function() { 101 | console.log('Success!'); 102 | }, 103 | function() { 104 | console.log('Error!'); 105 | }, 106 | 1337 107 | ); 108 | }); 109 | 110 | createActionButton('Stop Webserver', function() { 111 | console.log("Stopping webserver..."); 112 | 113 | webserver.stop( 114 | function() { 115 | console.log('Success!'); 116 | }, 117 | function() { 118 | console.log('Error!'); 119 | } 120 | ); 121 | }); 122 | }; 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cordova-plugin-webserver 2 | *A webserver plugin for cordova* 3 | 4 | This plugin helps you to start a full webserver in JavaScript on Android and iOS. 5 | 6 | ## Current supported platforms 7 | 8 | - Android (i think all versions?! Tell me if it's not true) 9 | - iOS (8.0 or later (armv7, armv7s or arm64)) 10 | 11 | ## Why? 12 | 13 | I started this project because I wanted a solution like [ExpressJS](http://expressjs.com/de/) but hybrid (for iOS and Android). I didn't want to build two native applications which each serve a backend (two codebases are just bah!). So this is the solution to create a Webserver which can receives HTTP Requests and responds with HTTP Responds. 14 | 15 | ## Installation 16 | 17 | Just add the cordova plugin to your project 18 | 19 | `cordova plugin add https://github.com/bykof/cordova-plugin-webserver` 20 | 21 | ## Use 22 | 23 | Ok so it's pretty ez. There are 4 Methods which are available in the `webserver` variable: 24 | 25 | - start(port) or start() 26 | - stop() 27 | - onRequest(request) 28 | - sendResponse(responseId, responseObject, callbackSuccess, callbackError) 29 | 30 | ### start(port) 31 | 32 | port (optional): Set a port of your webserver. 33 | Default port: 8080 34 | 35 | This method will start your webserver. 36 | 37 | ### stop() 38 | 39 | This method will stop your webserver. 40 | 41 | ### onRequest(callback(request)) 42 | 43 | Every time the webserver receives an request your callback function is called. 44 | The request params will look like this: 45 | ``` 46 | { 47 | requestId: '3jh4-ku34k-k3j4k-k3j42', 48 | body: '', 49 | headers: { 50 | ... some headers 51 | }, 52 | method: 'POST', 53 | path: '/hello/world', 54 | query: 'bla=wer&blu=2' 55 | } 56 | ``` 57 | 58 | ### sendResponse(requestId, responseObject, callbackSuccess, callbackError) 59 | 60 | If you received a request you will probably send a response "*cough*"... 61 | We need to add a requestId param to map a response to a request, because internally the webserver will wait for the response. (Yes currently the webserver will wait until there aren't computers anymore on earth). 62 | 63 | The params have to look like this (there are not default values for the params!): 64 | ``` 65 | { 66 | status: 200, 67 | body: 'Hello ... something', 68 | headers: { 69 | 'Content-Type': 'text/html' <--- this is important 70 | } 71 | } 72 | ``` 73 | 74 | #### Serving files 75 | 76 | 77 | To serve a file in response to an http request you should use `path` param which points to the file 78 | location on the device. 79 | 80 | ``` 81 | { 82 | status: 200, 83 | path: '/sdcard0/Downloads/whatever.txt', 84 | headers: { 85 | } 86 | } 87 | ``` 88 | 89 | The cordova-plugin-file plugin can be used to locate the path of the file data to be sent. For android you 90 | might need strip the `file://` part of the file path for it to work. 91 | ``` 92 | window.resolveLocalFileSystemURL('cdvfile://localhost/temporary/path/to/file.mp4', function(entry) { 93 | console.log(entry.toURL()); 94 | }); 95 | ``` 96 | 97 | ## Example 98 | 99 | ```javascript 100 | webserver.onRequest( 101 | function(request) { 102 | console.log("O MA GAWD! This is the request: ", request); 103 | 104 | webserver.sendResponse( 105 | request.requestId, 106 | { 107 | status: 200, 108 | body: 'Hello World', 109 | headers: { 110 | 'Content-Type': 'text/html' 111 | } 112 | } 113 | ); 114 | } 115 | ); 116 | 117 | webserver.start(); 118 | 119 | //... after a long long time 120 | // stop the server 121 | webserver.stop(); 122 | ``` 123 | 124 | ## Credits 125 | 126 | Special thanks to: 127 | 128 | - https://github.com/NanoHttpd/nanohttpd 129 | - https://github.com/swisspol/GCDWebServer 130 | -------------------------------------------------------------------------------- /src/android/Webserver.java: -------------------------------------------------------------------------------- 1 | package org.apache.cordova.plugin; 2 | 3 | import android.util.Log; 4 | 5 | import org.apache.cordova.*; 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | 9 | import java.io.IOException; 10 | import java.util.HashMap; 11 | 12 | 13 | public class Webserver extends CordovaPlugin { 14 | 15 | public HashMap responses; 16 | public CallbackContext onRequestCallbackContext; 17 | public NanoHTTPDWebserver nanoHTTPDWebserver; 18 | 19 | @Override 20 | public void initialize(CordovaInterface cordova, CordovaWebView webView) { 21 | super.initialize(cordova, webView); 22 | this.responses = new HashMap(); 23 | } 24 | 25 | @Override 26 | public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { 27 | if ("start".equals(action)) { 28 | try { 29 | this.start(args, callbackContext); 30 | } catch (IOException e) { 31 | e.printStackTrace(); 32 | } 33 | return true; 34 | } 35 | if ("stop".equals(action)) { 36 | this.stop(args, callbackContext); 37 | return true; 38 | } 39 | if ("onRequest".equals(action)) { 40 | this.onRequest(args, callbackContext); 41 | return true; 42 | } 43 | if ("sendResponse".equals(action)) { 44 | this.sendResponse(args, callbackContext); 45 | return true; 46 | } 47 | return false; // Returning false results in a "MethodNotFound" error. 48 | } 49 | 50 | /** 51 | * Starts the server 52 | * @param args 53 | * @param callbackContext 54 | */ 55 | private void start(JSONArray args, CallbackContext callbackContext) throws JSONException, IOException { 56 | int port = 8080; 57 | 58 | if (args.length() == 1) { 59 | port = args.getInt(0); 60 | } 61 | 62 | if (this.nanoHTTPDWebserver != null){ 63 | callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, "Server already running")); 64 | return; 65 | } 66 | 67 | try { 68 | this.nanoHTTPDWebserver = new NanoHTTPDWebserver(port, this); 69 | this.nanoHTTPDWebserver.start(); 70 | }catch (Exception e){ 71 | callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, e.getMessage())); 72 | return; 73 | } 74 | 75 | Log.d( 76 | this.getClass().getName(), 77 | "Server is running on: " + 78 | this.nanoHTTPDWebserver.getHostname() + ":" + 79 | this.nanoHTTPDWebserver.getListeningPort() 80 | ); 81 | callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 82 | } 83 | 84 | /** 85 | * Stops the server 86 | * @param args 87 | * @param callbackContext 88 | */ 89 | private void stop(JSONArray args, CallbackContext callbackContext) throws JSONException { 90 | if (this.nanoHTTPDWebserver != null) { 91 | this.nanoHTTPDWebserver.stop(); 92 | this.nanoHTTPDWebserver = null; 93 | } 94 | callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 95 | } 96 | 97 | /** 98 | * Will be called if the js context sends an response to the webserver 99 | * @param args {UUID: {...}} 100 | * @param callbackContext 101 | * @throws JSONException 102 | */ 103 | private void sendResponse(JSONArray args, CallbackContext callbackContext) throws JSONException { 104 | Log.d(this.getClass().getName(), "Got sendResponse: " + args.toString()); 105 | this.responses.put(args.getString(0), args.get(1)); 106 | callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK)); 107 | } 108 | 109 | /** 110 | * Just register the onRequest and send no result. This is needed to save the callbackContext to 111 | * invoke it later 112 | * @param args 113 | * @param callbackContext 114 | */ 115 | private void onRequest(JSONArray args, CallbackContext callbackContext) { 116 | this.onRequestCallbackContext = callbackContext; 117 | PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT); 118 | pluginResult.setKeepCallback(true); 119 | this.onRequestCallbackContext.sendPluginResult(pluginResult); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ios/Webserver.swift: -------------------------------------------------------------------------------- 1 | @objc(Webserver) class Webserver : CDVPlugin { 2 | // Timeout in seconds 3 | let TIMEOUT: Int = 60 * 3 * 1000000 4 | 5 | var webServer: GCDWebServer = GCDWebServer() 6 | var requests = SynchronizedDictionary() 7 | var onRequestCommand: CDVInvokedUrlCommand? = nil 8 | 9 | override func pluginInitialize() { 10 | self.webServer = GCDWebServer() 11 | self.onRequestCommand = nil 12 | self.requests = SynchronizedDictionary() 13 | self.initHTTPRequestHandlers() 14 | } 15 | 16 | func requestToRequestDict(requestUUID: String, request: GCDWebServerRequest) -> Dictionary { 17 | let dataRequest = request as! GCDWebServerDataRequest 18 | var body = "" 19 | 20 | if dataRequest.hasBody() { 21 | body = dataRequest.data.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) 22 | } 23 | 24 | return [ 25 | "requestId": requestUUID, 26 | "body": body, 27 | "headers": dataRequest.headers, 28 | "method": dataRequest.method, 29 | "path": dataRequest.path, 30 | "query": dataRequest.url.query ?? "", 31 | "bodyIsBase64": true // we only implement this for iOS so this way we can check if it is actually base64 32 | ] 33 | } 34 | 35 | func fileRequest(request: GCDWebServerRequest, path: String) -> GCDWebServerResponse { 36 | // Check if file exists, given its path 37 | if !(FileManager.default.fileExists(atPath: path)) { 38 | return GCDWebServerResponse(statusCode: 404); 39 | } 40 | 41 | if (request.hasByteRange()) { 42 | return GCDWebServerFileResponse(file: path, byteRange: request.byteRange)! 43 | } 44 | 45 | return GCDWebServerFileResponse(file: path)! 46 | } 47 | 48 | func processRequest(request: GCDWebServerRequest, completionBlock: @escaping GCDWebServerCompletionBlock) { 49 | if (self.onRequestCommand == nil) { 50 | 51 | print("No onRequest callback available. Ignore request") 52 | return 53 | } 54 | // Fetch data as GCDWebserverDataRequest 55 | let requestUUID = UUID().uuidString 56 | // Transform it into an dictionary for the javascript plugin 57 | let requestDict = self.requestToRequestDict(requestUUID: requestUUID, request: request) 58 | 59 | // Save the request to when we receive a response from javascript 60 | self.requests[requestUUID] = (request, completionBlock) 61 | 62 | // Do a call to the onRequestCommand to inform the JS plugin 63 | let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: requestDict) 64 | pluginResult?.setKeepCallbackAs(true) 65 | self.commandDelegate.send( 66 | pluginResult, 67 | callbackId: self.onRequestCommand?.callbackId 68 | ) 69 | } 70 | 71 | @objc(onRequest:) 72 | func onRequest(_ command: CDVInvokedUrlCommand) { 73 | self.onRequestCommand = command 74 | let pluginResult = CDVPluginResult(status: CDVCommandStatus_NO_RESULT) 75 | pluginResult?.setKeepCallbackAs(true) 76 | self.commandDelegate.send( 77 | pluginResult, 78 | callbackId: self.onRequestCommand?.callbackId 79 | ) 80 | } 81 | 82 | func initHTTPRequestHandlers() { 83 | self.webServer.addHandler( 84 | match: { 85 | (requestMethod, requestURL, requestHeaders, urlPath, urlQuery) -> GCDWebServerRequest? in 86 | return GCDWebServerDataRequest(method: requestMethod, url: requestURL, headers: requestHeaders, path: urlPath, query: urlQuery) 87 | }, 88 | asyncProcessBlock: self.processRequest 89 | ) 90 | } 91 | 92 | @objc(sendResponse:) 93 | func sendResponse(_ command: CDVInvokedUrlCommand) { 94 | do { 95 | let requestUUID = command.argument(at: 0) as! String 96 | 97 | if (self.requests[requestUUID] == nil) { 98 | print("No matching request") 99 | self.commandDelegate!.send(CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: "No matching request"), callbackId: command.callbackId) 100 | return 101 | } 102 | 103 | // We got the dict so put information in the response 104 | let request = self.requests[requestUUID]?.0 as! GCDWebServerRequest 105 | let completionBlock = self.requests[requestUUID]?.1 as! GCDWebServerCompletionBlock 106 | let responseDict = command.argument(at: 1) as! Dictionary 107 | 108 | // Check if a file path is provided else use regular data response 109 | let response = responseDict["path"] != nil 110 | ? fileRequest(request: request, path: responseDict["path"] as! String) 111 | : GCDWebServerDataResponse(text: responseDict["body"] as! String) 112 | 113 | if responseDict["status"] != nil { 114 | response?.statusCode = responseDict["status"] as! Int 115 | } 116 | 117 | for (key, value) in (responseDict["headers"] as! Dictionary) { 118 | response?.setValue(value, forAdditionalHeader: key) 119 | } 120 | 121 | // Remove the handled request 122 | self.requests.removeValue(forKey: requestUUID) 123 | 124 | // Complete the async response 125 | completionBlock(response!) 126 | 127 | let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK) 128 | self.commandDelegate!.send(pluginResult, callbackId: command.callbackId) 129 | } catch let error { 130 | print(error.localizedDescription) 131 | self.commandDelegate!.send(CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: error.localizedDescription), callbackId: command.callbackId) 132 | } 133 | } 134 | 135 | @objc(start:) 136 | func start(_ command: CDVInvokedUrlCommand) { 137 | var port = 8080 138 | let portArgument = command.argument(at: 0) 139 | 140 | if portArgument != nil { 141 | port = portArgument as! Int 142 | } 143 | 144 | if self.webServer.isRunning{ 145 | self.commandDelegate!.send(CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: "Server already running"), callbackId: command.callbackId) 146 | return 147 | } 148 | 149 | do { 150 | try self.webServer.start(options:[GCDWebServerOption_AutomaticallySuspendInBackground : false, GCDWebServerOption_Port: UInt(port)]) 151 | } catch let error { 152 | print(error.localizedDescription) 153 | self.commandDelegate!.send(CDVPluginResult(status: CDVCommandStatus_ERROR, messageAs: error.localizedDescription), callbackId: command.callbackId) 154 | return 155 | } 156 | let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK) 157 | self.commandDelegate!.send(pluginResult, callbackId: command.callbackId) 158 | } 159 | 160 | 161 | @objc(stop:) 162 | func stop(_ command: CDVInvokedUrlCommand) { 163 | if self.webServer.isRunning { 164 | self.webServer.stop() 165 | } 166 | print("Stopping webserver") 167 | let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK) 168 | self.commandDelegate!.send(pluginResult, callbackId: command.callbackId) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/android/NanoHTTPDWebserver.java: -------------------------------------------------------------------------------- 1 | package org.apache.cordova.plugin; 2 | 3 | 4 | import android.net.Uri; 5 | import android.util.Log; 6 | import android.webkit.MimeTypeMap; 7 | 8 | import org.apache.cordova.PluginResult; 9 | import org.json.JSONException; 10 | import org.json.JSONObject; 11 | 12 | import java.io.File; 13 | import java.io.FileInputStream; 14 | import java.io.FileNotFoundException; 15 | import java.io.IOException; 16 | import java.lang.reflect.Method; 17 | import java.util.HashMap; 18 | import java.util.Iterator; 19 | import java.util.Map; 20 | import java.util.UUID; 21 | 22 | import fi.iki.elonen.NanoHTTPD; 23 | 24 | public class NanoHTTPDWebserver extends NanoHTTPD { 25 | 26 | Webserver webserver; 27 | 28 | public NanoHTTPDWebserver(int port, Webserver webserver) { 29 | super(port); 30 | this.webserver = webserver; 31 | } 32 | 33 | private String getBodyText(IHTTPSession session) { 34 | Map files = new HashMap(); 35 | Method method = session.getMethod(); 36 | if (Method.PUT.equals(method) || Method.POST.equals(method)) { 37 | try { 38 | session.parseBody(files); 39 | } catch (IOException ioe) { 40 | return "{}"; 41 | } catch (ResponseException re) { 42 | return "{}"; 43 | } 44 | } 45 | // get the POST body 46 | return files.get("postData"); 47 | } 48 | 49 | /** 50 | * Create a request object 51 | *

52 | * [ 53 | * "requestId": requestUUID, 54 | * " body": request.jsonObject ?? "", 55 | * " headers": request.headers, 56 | * " method": request.method, 57 | * " path": request.url.path, 58 | * " query": request.url.query ?? "" 59 | * ] 60 | * 61 | * @param session 62 | * @return 63 | */ 64 | private JSONObject createJSONRequest(String requestId, IHTTPSession session) throws JSONException { 65 | JSONObject jsonRequest = new JSONObject(); 66 | jsonRequest.put("requestId", requestId); 67 | jsonRequest.put("body", this.getBodyText(session)); 68 | jsonRequest.put("headers", session.getHeaders()); 69 | jsonRequest.put("method", session.getMethod()); 70 | jsonRequest.put("path", session.getUri()); 71 | jsonRequest.put("query", session.getQueryParameterString()); 72 | return jsonRequest; 73 | } 74 | 75 | private String getContentType(JSONObject responseObject) throws JSONException { 76 | if (responseObject.has("headers") && 77 | responseObject.getJSONObject("headers").has("Content-Type")) { 78 | return responseObject.getJSONObject("headers").getString("Content-Type"); 79 | } else { 80 | return "text/plain"; 81 | } 82 | } 83 | 84 | private Response newFixedFileResponse(File file, String mime) throws FileNotFoundException { 85 | Response res; 86 | res = newFixedLengthResponse(Response.Status.OK, mime, new FileInputStream(file), (int) file.length()); 87 | res.addHeader("Accept-Ranges", "bytes"); 88 | return res; 89 | } 90 | 91 | Response serveFile(Map header, File file, String mime) { 92 | Response res; 93 | try { 94 | // Calculate etag 95 | String etag = Integer.toHexString((file.getAbsolutePath() + file.lastModified() + "" + file.length()).hashCode()); 96 | 97 | // Support (simple) skipping: 98 | long startFrom = 0; 99 | long endAt = -1; 100 | String range = header.get("range"); 101 | if (range != null) { 102 | if (range.startsWith("bytes=")) { 103 | range = range.substring("bytes=".length()); 104 | int minus = range.indexOf('-'); 105 | try { 106 | if (minus > 0) { 107 | startFrom = Long.parseLong(range.substring(0, minus)); 108 | endAt = Long.parseLong(range.substring(minus + 1)); 109 | } 110 | } catch (NumberFormatException ignored) { 111 | } 112 | } 113 | } 114 | 115 | // get if-range header. If present, it must match etag or else we 116 | // should ignore the range request 117 | String ifRange = header.get("if-range"); 118 | boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange)); 119 | 120 | String ifNoneMatch = header.get("if-none-match"); 121 | boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null && ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag)); 122 | 123 | // Change return code and add Content-Range header when skipping is 124 | // requested 125 | long fileLen = file.length(); 126 | 127 | if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) { 128 | // range request that matches current etag 129 | // and the startFrom of the range is satisfiable 130 | if (headerIfNoneMatchPresentAndMatching) { 131 | // range request that matches current etag 132 | // and the startFrom of the range is satisfiable 133 | // would return range from file 134 | // respond with not-modified 135 | res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); 136 | res.addHeader("ETag", etag); 137 | } else { 138 | if (endAt < 0) { 139 | endAt = fileLen - 1; 140 | } 141 | long newLen = endAt - startFrom + 1; 142 | if (newLen < 0) { 143 | newLen = 0; 144 | } 145 | 146 | FileInputStream fis = new FileInputStream(file); 147 | fis.skip(startFrom); 148 | 149 | res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, fis, newLen); 150 | res.addHeader("Accept-Ranges", "bytes"); 151 | res.addHeader("Content-Length", "" + newLen); 152 | res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen); 153 | res.addHeader("ETag", etag); 154 | } 155 | } else { 156 | 157 | if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) { 158 | // return the size of the file 159 | // 4xx responses are not trumped by if-none-match 160 | res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, ""); 161 | res.addHeader("Content-Range", "bytes */" + fileLen); 162 | res.addHeader("ETag", etag); 163 | } else if (range == null && headerIfNoneMatchPresentAndMatching) { 164 | // full-file-fetch request 165 | // would return entire file 166 | // respond with not-modified 167 | res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); 168 | res.addHeader("ETag", etag); 169 | } else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) { 170 | // range request that doesn't match current etag 171 | // would return entire (different) file 172 | // respond with not-modified 173 | 174 | res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, ""); 175 | res.addHeader("ETag", etag); 176 | } else { 177 | // supply the file 178 | res = newFixedFileResponse(file, mime); 179 | res.addHeader("Content-Length", "" + fileLen); 180 | res.addHeader("ETag", etag); 181 | } 182 | } 183 | } catch (IOException ioe) { 184 | res = newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, ioe.getMessage()); 185 | } 186 | 187 | return res; 188 | } 189 | 190 | /** 191 | * Get mime type based of file extension 192 | * @param url 193 | * @return 194 | */ 195 | public static String getMimeType(String url) { 196 | String type = null; 197 | String extension = MimeTypeMap.getFileExtensionFromUrl(url); 198 | if (extension != null) { 199 | type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 200 | } 201 | return type; 202 | } 203 | 204 | @Override 205 | public Response serve(IHTTPSession session) { 206 | Log.d(this.getClass().getName(), "New request is incoming!"); 207 | 208 | String requestUUID = UUID.randomUUID().toString(); 209 | 210 | PluginResult pluginResult = null; 211 | try { 212 | pluginResult = new PluginResult( 213 | PluginResult.Status.OK, this.createJSONRequest(requestUUID, session)); 214 | } catch (JSONException e) { 215 | e.printStackTrace(); 216 | } 217 | pluginResult.setKeepCallback(true); 218 | this.webserver.onRequestCallbackContext.sendPluginResult(pluginResult); 219 | 220 | while (!this.webserver.responses.containsKey(requestUUID)) { 221 | try { 222 | Thread.sleep(1); 223 | } catch (InterruptedException e) { 224 | e.printStackTrace(); 225 | } 226 | } 227 | 228 | JSONObject responseObject = (JSONObject) this.webserver.responses.get(requestUUID); 229 | Response response = null; 230 | Log.d(this.getClass().getName(), "responseObject: " + responseObject.toString()); 231 | 232 | if (responseObject.has("path")) { 233 | try { 234 | File file = new File(responseObject.getString("path")); 235 | Uri uri = Uri.fromFile(file); 236 | String mime = getMimeType(uri.toString()); 237 | Response res = serveFile(session.getHeaders(), file, mime); 238 | Iterator keys = responseObject.getJSONObject("headers").keys(); 239 | while (keys.hasNext()) { 240 | String key = (String) keys.next(); 241 | res.addHeader( 242 | key, 243 | responseObject.getJSONObject("headers").getString(key) 244 | ); 245 | } 246 | return res; 247 | } catch (JSONException e) { 248 | e.printStackTrace(); 249 | } 250 | return response; 251 | } else { 252 | try { 253 | response = newFixedLengthResponse( 254 | Response.Status.lookup(responseObject.getInt("status")), 255 | getContentType(responseObject), 256 | responseObject.getString("body") 257 | ); 258 | 259 | Iterator keys = responseObject.getJSONObject("headers").keys(); 260 | while (keys.hasNext()) { 261 | String key = (String) keys.next(); 262 | response.addHeader( 263 | key, 264 | responseObject.getJSONObject("headers").getString(key) 265 | ); 266 | } 267 | 268 | } catch (JSONException e) { 269 | e.printStackTrace(); 270 | } 271 | return response; 272 | } 273 | } 274 | } 275 | --------------------------------------------------------------------------------