From c50679be793fea248323eaf623bebddb148a0542 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 12:12:35 +0100 Subject: [PATCH 01/15] Adding the ability to provide custom paths for serving multiple paths from a single server. --- src/android/CorHttpd.java | 29 +++++++++++++++++++++++-- src/android/WebServer.java | 44 ++++++++++++++++++++++++++++++++++++-- www/CorHttpd.js | 3 ++- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/android/CorHttpd.java b/src/android/CorHttpd.java index 903a36b..27630bf 100644 --- a/src/android/CorHttpd.java +++ b/src/android/CorHttpd.java @@ -7,6 +7,9 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CallbackContext; @@ -42,6 +45,7 @@ public class CorHttpd extends CordovaPlugin { private static final String OPT_WWW_ROOT = "www_root"; private static final String OPT_PORT = "port"; private static final String OPT_LOCALHOST_ONLY = "localhost_only"; + private static final String OPT_CUSTOM_PATHS = "custom_paths"; private String www_root = ""; private int port = 8888; @@ -50,6 +54,8 @@ public class CorHttpd extends CordovaPlugin { private String localPath = ""; private WebServer server = null; private String url = ""; + private Map customPaths = null; + private JSONObject jsonCustomPaths = null; @Override public boolean execute(String action, JSONArray inputs, CallbackContext callbackContext) throws JSONException { @@ -107,6 +113,7 @@ private PluginResult startServer(JSONArray inputs, CallbackContext callbackConte www_root = options.optString(OPT_WWW_ROOT); port = options.optInt(OPT_PORT, 8888); localhost_only = options.optBoolean(OPT_LOCALHOST_ONLY, false); + jsonCustomPaths = options.optJSONObject(OPT_CUSTOM_PATHS); if(www_root.startsWith("/")) { //localPath = Environment.getExternalStorageDirectory().getAbsolutePath(); @@ -146,11 +153,29 @@ private String __startServer() { AssetManager am = ctx.getResources().getAssets(); f.setAssetManager( am ); + if (jsonCustomPaths != null) { + Iterator keys = jsonCustomPaths.keys(); + if (keys != null) { + customPaths = new HashMap(jsonCustomPaths.length()); + while (keys.hasNext()) { + String key = (String) keys.next(); + String path = jsonCustomPaths.optString(key); + if (!path.startsWith("/")) { + path = "www" + path.length() > 0 ? "/" + path : ""; + } + Log.w(LOGTAG, "Custom URL - " + key + " - " + path); + AndroidFile p = new AndroidFile(path); + p.setAssetManager( am ); + customPaths.put(key, p); + } + } + } + if(localhost_only) { InetSocketAddress localAddr = InetSocketAddress.createUnresolved("127.0.0.1", port); - server = new WebServer(localAddr, f); + server = new WebServer(localAddr, f, customPaths); } else { - server = new WebServer(port, f); + server = new WebServer(port, f, customPaths); } } catch (IOException e) { errmsg = String.format("IO Exception: %s", e.getMessage()); diff --git a/src/android/WebServer.java b/src/android/WebServer.java index 0c35ca3..fd0c5c1 100644 --- a/src/android/WebServer.java +++ b/src/android/WebServer.java @@ -1,15 +1,55 @@ package com.rjfun.cordova.httpd; import java.io.IOException; +import java.lang.String; import java.net.InetSocketAddress; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; public class WebServer extends NanoHTTPD { - public WebServer(InetSocketAddress localAddr, AndroidFile wwwroot) throws IOException { + private Map customPaths = null; + private String[] customURIs = String[]; + private final String LOGTAG = "NanoHTTPD-Cordova"; + + public WebServer(InetSocketAddress localAddr, AndroidFile wwwroot, Map customPaths ) throws IOException { super(localAddr, wwwroot); + addCustomPaths(customPaths); } - public WebServer(int port, AndroidFile wwwroot ) throws IOException { + public WebServer(int port, AndroidFile wwwroot, Map customPaths ) throws IOException { super(port, wwwroot); + addCustomPaths(customPaths); } + + private addCustomPaths(Map customPaths) { + this.customPaths = customPaths; + customURIs = new String[customPaths.keySet().size()]; + int i = 0; + Iterator keys = customPaths.keySet().iterator(); + while (keys.hasNext()) { + customURIs[i++] = (String) keys.next(); + } + } + + public Response serve( String uri, String method, Properties header, Properties parms, Properties files ) + { + if (customURIs.length > 0) { + int i = 0; + for (i = 0; i < customURIs.length; i++) { + String testURI = customURIs[i]; + if (uri.startsWith(testURI)) { + Log.i( LOGTAG, method + " '" + uri + "' " ); + String newURI = uri.substring(testURI.length()); + return serveFile( newURI, header, customPaths.get(testURI), true ); + } + } + if (i == customURIs.length) { + super( uri, method, header, parms, files ); + } + } else { + super( uri, method, header, parms, files ); + } + } } diff --git a/www/CorHttpd.js b/www/CorHttpd.js index 0c6f82c..9ae58d2 100644 --- a/www/CorHttpd.js +++ b/www/CorHttpd.js @@ -8,7 +8,8 @@ corhttpd_exports.startServer = function(options, success, error) { var defaults = { 'www_root': '', 'port': 8888, - 'localhost_only': false + 'localhost_only': false, + 'custom_paths': {} }; // Merge optional settings into defaults. From ea1b17e95fda5b997508642359c87b4b27b34c25 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 12:15:42 +0100 Subject: [PATCH 02/15] Missing method type declaration --- src/android/WebServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/WebServer.java b/src/android/WebServer.java index fd0c5c1..0c91e40 100644 --- a/src/android/WebServer.java +++ b/src/android/WebServer.java @@ -23,7 +23,7 @@ public WebServer(int port, AndroidFile wwwroot, Map customPaths ) throws IOExcep addCustomPaths(customPaths); } - private addCustomPaths(Map customPaths) { + private void addCustomPaths(Map customPaths) { this.customPaths = customPaths; customURIs = new String[customPaths.keySet().size()]; int i = 0; From 17f00621baa5a2a75e2b12251f128e269df51a1f Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 12:17:50 +0100 Subject: [PATCH 03/15] Wrong String array declaration. --- src/android/WebServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/WebServer.java b/src/android/WebServer.java index 0c91e40..fcad29f 100644 --- a/src/android/WebServer.java +++ b/src/android/WebServer.java @@ -10,7 +10,7 @@ public class WebServer extends NanoHTTPD { private Map customPaths = null; - private String[] customURIs = String[]; + private String[] customURIs = new String[0]; private final String LOGTAG = "NanoHTTPD-Cordova"; public WebServer(InetSocketAddress localAddr, AndroidFile wwwroot, Map customPaths ) throws IOException { From 081fab1202a946759ef4b5b7aa0a3977cd81f5ef Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 13:11:07 +0100 Subject: [PATCH 04/15] Fixing broken import, path determination, and bad method calls --- src/android/CorHttpd.java | 4 +++- src/android/WebServer.java | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/android/CorHttpd.java b/src/android/CorHttpd.java index 27630bf..a0c748c 100644 --- a/src/android/CorHttpd.java +++ b/src/android/CorHttpd.java @@ -161,7 +161,9 @@ private String __startServer() { String key = (String) keys.next(); String path = jsonCustomPaths.optString(key); if (!path.startsWith("/")) { - path = "www" + path.length() > 0 ? "/" + path : ""; + if (path.length() > 0) { + path = "www" + "/" + path; + } } Log.w(LOGTAG, "Custom URL - " + key + " - " + path); AndroidFile p = new AndroidFile(path); diff --git a/src/android/WebServer.java b/src/android/WebServer.java index fcad29f..4741ef3 100644 --- a/src/android/WebServer.java +++ b/src/android/WebServer.java @@ -7,6 +7,8 @@ import java.util.Map; import java.util.Properties; +import android.util.Log; + public class WebServer extends NanoHTTPD { private Map customPaths = null; @@ -42,14 +44,14 @@ public Response serve( String uri, String method, Properties header, Properties if (uri.startsWith(testURI)) { Log.i( LOGTAG, method + " '" + uri + "' " ); String newURI = uri.substring(testURI.length()); - return serveFile( newURI, header, customPaths.get(testURI), true ); + return serveFile( newURI, header, (AndroidFile) customPaths.get(testURI), true ); } } if (i == customURIs.length) { - super( uri, method, header, parms, files ); + super.serve( uri, method, header, parms, files ); } } else { - super( uri, method, header, parms, files ); + super.serve( uri, method, header, parms, files ); } } } From f48bb370f75c41f6cbc8f1460bba94f4424cb520 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 13:16:15 +0100 Subject: [PATCH 05/15] Not returning values in a method when I should have done. --- src/android/WebServer.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/android/WebServer.java b/src/android/WebServer.java index 4741ef3..be20000 100644 --- a/src/android/WebServer.java +++ b/src/android/WebServer.java @@ -38,8 +38,7 @@ private void addCustomPaths(Map customPaths) { public Response serve( String uri, String method, Properties header, Properties parms, Properties files ) { if (customURIs.length > 0) { - int i = 0; - for (i = 0; i < customURIs.length; i++) { + for (int i = 0; i < customURIs.length; i++) { String testURI = customURIs[i]; if (uri.startsWith(testURI)) { Log.i( LOGTAG, method + " '" + uri + "' " ); @@ -47,11 +46,10 @@ public Response serve( String uri, String method, Properties header, Properties return serveFile( newURI, header, (AndroidFile) customPaths.get(testURI), true ); } } - if (i == customURIs.length) { - super.serve( uri, method, header, parms, files ); + return super.serve( uri, method, header, parms, files ); } } else { - super.serve( uri, method, header, parms, files ); + return super.serve( uri, method, header, parms, files ); } } } From 4731a766f5d1968ff1a323cb4ef20fcd0ba677c2 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 13:18:26 +0100 Subject: [PATCH 06/15] Dodgy hanging brace --- src/android/WebServer.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/android/WebServer.java b/src/android/WebServer.java index be20000..0f836b6 100644 --- a/src/android/WebServer.java +++ b/src/android/WebServer.java @@ -47,7 +47,6 @@ public Response serve( String uri, String method, Properties header, Properties } } return super.serve( uri, method, header, parms, files ); - } } else { return super.serve( uri, method, header, parms, files ); } From 89cda0ffcd2e8b4989dd35033e03bb2c20320c52 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 13:30:13 +0100 Subject: [PATCH 07/15] Updating the readme.md file for description on how to use the custom paths. --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ce24136..a2916b5 100644 --- a/README.md +++ b/README.md @@ -79,12 +79,18 @@ Example code: (read the comments) * if a relative path is given, it will be relative to cordova assets/www/ in APK. * "", by default, it will point to cordova assets/www/, it's good to use 'htdocs' for 'www/htdocs' * if a absolute path is given, it will access file system. - * "/", set the root dir as the www root, it maybe a security issue, but very powerful to browse all dir + * "/", set the root dir as the www root, it maybe a security issue, but very powerful to browse all dir. + * Note the use of custom_paths (which is entirely optionaly) allows you to specify different base + * URLs, and where to serve the content from for that URL. This allows you for example to serve + * content from both the read only app storage area, as well as the read-write data directory. */ httpd.startServer({ 'www_root' : wwwroot, 'port' : 8080, - 'localhost_only' : false + 'localhost_only' : false, + 'custom_paths' : { + '/rw/' : cordova.file.dataDirectory.substring(7) + } }, function( url ){ // if server is up, it will return the url of http://:port/ // the ip is the active network connection From 4f48d8fe0d8ef82d453faca7d913e7ae65bbfea4 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 16:07:34 +0100 Subject: [PATCH 08/15] Adding debug info and simplifying the serve function. --- src/android/WebServer.java | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/android/WebServer.java b/src/android/WebServer.java index 0f836b6..bb61834 100644 --- a/src/android/WebServer.java +++ b/src/android/WebServer.java @@ -31,24 +31,23 @@ private void addCustomPaths(Map customPaths) { int i = 0; Iterator keys = customPaths.keySet().iterator(); while (keys.hasNext()) { - customURIs[i++] = (String) keys.next(); + String path = (String) keys.next(); + customURIs[i] = path; + i++; + Log.i( LOGTAG, "Custom Path: " + path); } } public Response serve( String uri, String method, Properties header, Properties parms, Properties files ) { - if (customURIs.length > 0) { - for (int i = 0; i < customURIs.length; i++) { - String testURI = customURIs[i]; - if (uri.startsWith(testURI)) { - Log.i( LOGTAG, method + " '" + uri + "' " ); - String newURI = uri.substring(testURI.length()); - return serveFile( newURI, header, (AndroidFile) customPaths.get(testURI), true ); - } + for (int i = 0; i < customURIs.length; i++) { + String testURI = customURIs[i]; + if (uri.startsWith(testURI)) { + Log.i( LOGTAG, method + " '" + uri + "' " ); + String newURI = uri.substring(testURI.length()); + return serveFile( newURI, header, (AndroidFile) customPaths.get(testURI), true ); } - return super.serve( uri, method, header, parms, files ); - } else { - return super.serve( uri, method, header, parms, files ); } + return super.serve( uri, method, header, parms, files ); } } From 8acbc23d25d341d08c385e52c9dd18d8954d6e37 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 20:09:16 +0100 Subject: [PATCH 09/15] app was crashing in Android with an NPE occasionally. According to logical, the NPE was due to the method being null. Reversing the order of the equality check covers this. --- src/android/NanoHTTPD.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/android/NanoHTTPD.java b/src/android/NanoHTTPD.java index d249309..0585f23 100644 --- a/src/android/NanoHTTPD.java +++ b/src/android/NanoHTTPD.java @@ -427,7 +427,7 @@ else if (splitbyte==0 || size == 0x7FFFFFFFFFFFFFFFl) // If the method is POST, there may be parameters // in data section, too, read it: - if ( method.equalsIgnoreCase( "POST" )) + if ( "POST".equalsIgnoreCase( method )) { String contentType = ""; String contentTypeHeader = header.getProperty("content-type"); @@ -466,7 +466,7 @@ else if (splitbyte==0 || size == 0x7FFFFFFFFFFFFFFFl) } } - if ( method.equalsIgnoreCase( "PUT" )) + if ( "PUT".equalsIgnoreCase( method )) files.put("content", saveTmpFile( fbuf, 0, f.size())); // Ok, now do the serve() From 16038c9a510e5ae903b13cf04a34f5b2ef38c181 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 20:27:16 +0100 Subject: [PATCH 10/15] Fixing the ability to set the root of the custom path to be the www folder. --- src/android/CorHttpd.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/android/CorHttpd.java b/src/android/CorHttpd.java index a0c748c..0f48dff 100644 --- a/src/android/CorHttpd.java +++ b/src/android/CorHttpd.java @@ -162,7 +162,9 @@ private String __startServer() { String path = jsonCustomPaths.optString(key); if (!path.startsWith("/")) { if (path.length() > 0) { - path = "www" + "/" + path; + path = "www/" + path; + } else { + path = "www/"; } } Log.w(LOGTAG, "Custom URL - " + key + " - " + path); From ae5865b527fa6312c75fe3b2de0c7ad5a5a0ad61 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Apr 2015 20:40:32 +0100 Subject: [PATCH 11/15] Try www instead of www/ for the path. --- src/android/CorHttpd.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/CorHttpd.java b/src/android/CorHttpd.java index 0f48dff..22ec5b0 100644 --- a/src/android/CorHttpd.java +++ b/src/android/CorHttpd.java @@ -164,7 +164,7 @@ private String __startServer() { if (path.length() > 0) { path = "www/" + path; } else { - path = "www/"; + path = "www"; } } Log.w(LOGTAG, "Custom URL - " + key + " - " + path); From e33ecd1c0dc38e6c53c8dc807cf938a6c3137a3b Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Fri, 24 Apr 2015 15:59:58 +0100 Subject: [PATCH 12/15] Adding support for custom paths in IOS --- plugin.xml | 2 + src/ios/CorHttpd.m | 28 +++++++ src/ios/CustomPathHTTPConnection.h | 7 ++ src/ios/CustomPathHTTPConnection.m | 122 +++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 src/ios/CustomPathHTTPConnection.h create mode 100644 src/ios/CustomPathHTTPConnection.m diff --git a/plugin.xml b/plugin.xml index e95bf71..ecd48ff 100644 --- a/plugin.xml +++ b/plugin.xml @@ -50,6 +50,8 @@ xmlns:android="http://schemas.android.com/apk/res/android"> + + diff --git a/src/ios/CorHttpd.m b/src/ios/CorHttpd.m index 3a1b0c4..633315d 100644 --- a/src/ios/CorHttpd.m +++ b/src/ios/CorHttpd.m @@ -5,6 +5,7 @@ #import "DDLog.h" #import "DDTTYLogger.h" #import "HTTPServer.h" +#import "CustomPathHTTPConnection.h" @interface CorHttpd : CDVPlugin { // Member variables go here. @@ -14,6 +15,7 @@ @interface CorHttpd : CDVPlugin { @property(nonatomic, retain) HTTPServer *httpServer; @property(nonatomic, retain) NSString *localPath; @property(nonatomic, retain) NSString *url; +@property(nonatomic, retain) NSMutableDictionary *customPaths; @property (nonatomic, retain) NSString* www_root; @property (assign) int port; @@ -43,6 +45,7 @@ @implementation CorHttpd #define OPT_WWW_ROOT @"www_root" #define OPT_PORT @"port" #define OPT_LOCALHOST_ONLY @"localhost_only" +#define OPT_CUSTOM_PATHS @"custom_paths" #define IP_LOCALHOST @"127.0.0.1" #define IP_ANY @"0.0.0.0" @@ -142,6 +145,31 @@ - (void)startServer:(CDVInvokedUrlCommand*)command [DDLog addLogger:[DDTTYLogger sharedInstance]]; self.httpServer = [[HTTPServer alloc] init]; + [self.httpServer setConnectionClass:[CustomPathHTTPConnection class]]; + + NSDictionary* customPathsFromOptions = (NSDictionary *)[options valueForKey:OPT_CUSTOM_PATHS]; + if (customPathsFromOptions == nil) { + self.customPaths = [NSMutableDictionary dictionaryWithCapacity:0]; + } else { + self.customPaths = [NSMutableDictionary dictionaryWithCapacity:[customPathsFromOptions count]]; + [customPathsFromOptions enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) + { + NSString* customPath = (NSString*) key; + NSString* path = (NSString*) obj; + NSString* localPath = nil; + const char * docroot = [path UTF8String]; + if(*docroot == '/') { + localPath = path; + } else { + NSString* basePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"www"]; + localPath = [NSString stringWithFormat:@"%@/%@", basePath, path]; + } + NSLog(@"Custom Path: %@ - %@", customPath, localPath); + [self.customPaths setObject:localPath forKey:customPath]; + }]; + } + [CustomPathHTTPConnection setCustomPaths:self.customPaths]; + // Tell the server to broadcast its presence via Bonjour. // This allows browsers such as Safari to automatically discover our service. //[self.httpServer setType:@"_http._tcp."]; diff --git a/src/ios/CustomPathHTTPConnection.h b/src/ios/CustomPathHTTPConnection.h new file mode 100644 index 0000000..978143c --- /dev/null +++ b/src/ios/CustomPathHTTPConnection.h @@ -0,0 +1,7 @@ +#import "HTTPConnection.h" + +@interface CustomPathHTTPConnection : HTTPConnection ++ (NSDictionary *) customPaths; ++ (void) setCustomPaths:(NSDictionary *) cusPaths; +- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory documentRoot:(NSString *) documentRoot; +@end \ No newline at end of file diff --git a/src/ios/CustomPathHTTPConnection.m b/src/ios/CustomPathHTTPConnection.m new file mode 100644 index 0000000..4d298f1 --- /dev/null +++ b/src/ios/CustomPathHTTPConnection.m @@ -0,0 +1,122 @@ +#import +#import "HTTPConnection.h" +#import "HTTPLogging.h" + +@implementation CustomPathHTTPConnection : HTTPConnection +static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; +static NSDictionary * customPaths = nil; ++ (NSDictionary *) customPaths { @synchronized(self) { return customPaths; } } ++ (void) setCustomPaths:(NSDictionary *) cusPaths { @synchronized(self) { customPaths = cusPaths; } } + +- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory +{ + HTTPLogTrace(); + __block NSString *pathForURI = nil; + [CustomPathHTTPConnection.customPaths enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) + { + NSString* customPath = (NSString*) key; + if ([path hasPrefix:customPath]) { + *stop = YES; + NSString* subPath = [path substringFromIndex:[customPath length]]; + pathForURI = [self filePathForURI:subPath allowDirectory:allowDirectory documentRoot:(NSString *) obj]; + } + }]; + if (pathForURI != nil) { + return pathForURI; + } else { + return [super filePathForURI:path allowDirectory:allowDirectory]; + } +} + +- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory documentRoot:(NSString *) documentRoot +{ + // Part 0: Validate document root setting. + // + // If there is no configured documentRoot, + // then it makes no sense to try to return anything. + + if (documentRoot == nil) + { + HTTPLogWarn(@"%@[%p]: No configured document root", THIS_FILE, self); + return nil; + } + + // Part 1: Strip parameters from the url + // + // E.g.: /page.html?q=22&var=abc -> /page.html + + NSURL *docRoot = [NSURL fileURLWithPath:documentRoot isDirectory:YES]; + if (docRoot == nil) + { + HTTPLogWarn(@"%@[%p]: Document root is invalid file path", THIS_FILE, self); + return nil; + } + + NSString *relativePath = [[NSURL URLWithString:path relativeToURL:docRoot] relativePath]; + + // Part 2: Append relative path to document root (base path) + // + // E.g.: relativePath="/images/icon.png" + // documentRoot="/Users/robbie/Sites" + // fullPath="/Users/robbie/Sites/images/icon.png" + // + // We also standardize the path. + // + // E.g.: "Users/robbie/Sites/images/../index.html" -> "/Users/robbie/Sites/index.html" + + NSString *fullPath = [[documentRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath]; + + if ([relativePath isEqualToString:@"/"]) + { + fullPath = [fullPath stringByAppendingString:@"/"]; + } + + // Part 3: Prevent serving files outside the document root. + // + // Sneaky requests may include ".." in the path. + // + // E.g.: relativePath="../Documents/TopSecret.doc" + // documentRoot="/Users/robbie/Sites" + // fullPath="/Users/robbie/Documents/TopSecret.doc" + // + // E.g.: relativePath="../Sites_Secret/TopSecret.doc" + // documentRoot="/Users/robbie/Sites" + // fullPath="/Users/robbie/Sites_Secret/TopSecret" + + if (![documentRoot hasSuffix:@"/"]) + { + documentRoot = [documentRoot stringByAppendingString:@"/"]; + } + + if (![fullPath hasPrefix:documentRoot]) + { + HTTPLogWarn(@"%@[%p]: Request for file outside document root", THIS_FILE, self); + return nil; + } + + // Part 4: Search for index page if path is pointing to a directory + if (!allowDirectory) + { + BOOL isDir = NO; + if ([[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDir] && isDir) + { + NSArray *indexFileNames = [self directoryIndexFileNames]; + + for (NSString *indexFileName in indexFileNames) + { + NSString *indexFilePath = [fullPath stringByAppendingPathComponent:indexFileName]; + + if ([[NSFileManager defaultManager] fileExistsAtPath:indexFilePath isDirectory:&isDir] && !isDir) + { + return indexFilePath; + } + } + + // No matching index files found in directory + return nil; + } + } + + return fullPath; +} +@end \ No newline at end of file From e8bc4df5bfa41ca4b1cbdd361f9655b825a54ecc Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Fri, 24 Apr 2015 16:18:08 +0100 Subject: [PATCH 13/15] Trying to fix an IOS build issue. --- src/ios/CustomPathHTTPConnection.m | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ios/CustomPathHTTPConnection.m b/src/ios/CustomPathHTTPConnection.m index 4d298f1..b92c84b 100644 --- a/src/ios/CustomPathHTTPConnection.m +++ b/src/ios/CustomPathHTTPConnection.m @@ -1,6 +1,12 @@ #import #import "HTTPConnection.h" #import "HTTPLogging.h" + +@interface CustomPathHTTPConnection : HTTPConnection ++ (NSDictionary *) customPaths; ++ (void) setCustomPaths:(NSDictionary *) cusPaths; +- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory documentRoot:(NSString *) documentRoot; +@end @implementation CustomPathHTTPConnection : HTTPConnection static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; From d937b50dd157e233b66315744f55613cb6864b99 Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Wed, 22 Jul 2015 14:41:17 +0100 Subject: [PATCH 14/15] Allow the HTTPD server to work in a proxy mode. If you define a custom path pointing to http://... or https://... it will internally redirect the request to the remote server, and serve the response back to the user from the local server. For example: cordova.plugins.CorHttpd.startServer({ 'www_root': '', 'port': 8080, 'localhost_only': false, 'custom_paths': { '/imgur/': 'http://i.imgur.com/' } }, function(){console.log("SUCCESS");}, function(){console.log("FAILURE");}); When you then fetch http://localhost:8080/imgur/TNZYux0.jpg, you will fetch the content from imgur, and serve it as if it came from your own domain. The reason for doing this... CrossDomain issues on image content served from an off device domain, and not wanting to apply unrestricted CORS access to all domains. NOTE: On iOS, this will download the entire contents of the response into memory before sending it on to the calling client. On Android, this is chunked in up to 16kb segments. Someone with more iOS experience than myself can probably make the iOS side of this behave more efficiently. --- src/android/CorHttpd.java | 12 ++-- src/android/NanoHTTPD.java | 18 +++-- src/android/WebServer.java | 107 ++++++++++++++++++++++++++++- src/ios/CorHttpd.m | 2 +- src/ios/CustomPathHTTPConnection.h | 10 +++ src/ios/CustomPathHTTPConnection.m | 81 ++++++++++++++++++++-- 6 files changed, 211 insertions(+), 19 deletions(-) diff --git a/src/android/CorHttpd.java b/src/android/CorHttpd.java index 22ec5b0..421be4b 100644 --- a/src/android/CorHttpd.java +++ b/src/android/CorHttpd.java @@ -160,7 +160,7 @@ private String __startServer() { while (keys.hasNext()) { String key = (String) keys.next(); String path = jsonCustomPaths.optString(key); - if (!path.startsWith("/")) { + if (!path.startsWith("/") && !path.startsWith("http://") && !path.startsWith("https://")) { if (path.length() > 0) { path = "www/" + path; } else { @@ -168,9 +168,13 @@ private String __startServer() { } } Log.w(LOGTAG, "Custom URL - " + key + " - " + path); - AndroidFile p = new AndroidFile(path); - p.setAssetManager( am ); - customPaths.put(key, p); + if (path.startsWith("http://") || path.startsWith("https://" )) { + customPaths.put(key, path); + } else { + AndroidFile p = new AndroidFile(path); + p.setAssetManager(am); + customPaths.put(key, p); + } } } } diff --git a/src/android/NanoHTTPD.java b/src/android/NanoHTTPD.java index 0585f23..f9f6647 100644 --- a/src/android/NanoHTTPD.java +++ b/src/android/NanoHTTPD.java @@ -9,6 +9,7 @@ import java.io.OutputStream; import java.io.PrintStream; import java.io.PrintWriter; +import java.lang.InterruptedException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; @@ -832,15 +833,18 @@ private void sendResponse( String status, String mime, Properties header, InputS if ( data != null ) { - int pending = data.available(); // This is to support partial sends, see serveFile() byte[] buff = new byte[theBufferSize]; - while (pending>0) + int read; + while ((read = data.read( buff, 0, theBufferSize)) != -1 ) { - int read = data.read( buff, 0, ( (pending>theBufferSize) ? theBufferSize : pending )); - if (read <= 0) break; - out.write( buff, 0, read ); - pending -= read; - } + if (read == 0) { + try { + Thread.sleep(50); + } catch (InterruptedException e) {} + } else { + out.write(buff, 0, read); + } + } } out.flush(); out.close(); diff --git a/src/android/WebServer.java b/src/android/WebServer.java index bb61834..a53158c 100644 --- a/src/android/WebServer.java +++ b/src/android/WebServer.java @@ -1,12 +1,19 @@ package com.rjfun.cordova.httpd; +import java.io.BufferedInputStream; +import java.io.InputStream; import java.io.IOException; +import java.lang.Override; import java.lang.String; +import java.net.MalformedURLException; import java.net.InetSocketAddress; +import java.net.HttpURLConnection; +import java.net.URL; import java.util.Iterator; import java.util.Map; import java.util.Properties; + import android.util.Log; public class WebServer extends NanoHTTPD @@ -40,14 +47,112 @@ private void addCustomPaths(Map customPaths) { public Response serve( String uri, String method, Properties header, Properties parms, Properties files ) { + if (uri == null || method == null) { + return null; + } + Log.i( LOGTAG, method + " '" + uri + "' " ); for (int i = 0; i < customURIs.length; i++) { String testURI = customURIs[i]; if (uri.startsWith(testURI)) { Log.i( LOGTAG, method + " '" + uri + "' " ); String newURI = uri.substring(testURI.length()); - return serveFile( newURI, header, (AndroidFile) customPaths.get(testURI), true ); + Object customPath = customPaths.get(testURI); + if (customPath instanceof String) { + URL url = null; + HttpURLConnection connection = null; + InputStream in = null; + + // Open the HTTP connection + try { + url = new URL(((String) customPath) + newURI); + connection = (HttpURLConnection) url.openConnection(); + connection.connect(); + } catch (MalformedURLException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + } + try { + in = new InputStreamWithOverloadedClose(connection.getInputStream(), connection); + } catch (IOException e) { + e.printStackTrace(); + } + String datatype = connection.getContentType(); //NanoHTTPD.MIME_DEFAULT_BINARY + Response response = new NanoHTTPD.Response(NanoHTTPD.HTTP_OK, datatype, in); + if (connection.getContentEncoding() != null) + response.addHeader("Content-Encoding", connection.getContentEncoding()); + if (connection.getContentLength() != -1) + response.addHeader("Content-Length", "" + connection.getContentLength()); + if (connection.getHeaderField("Date") != null) + response.addHeader("Date", connection.getHeaderField("Date")); + if (connection.getHeaderField("Last-Modified") != null) + response.addHeader("Last-Modified", connection.getHeaderField("Last-Modified")); + if (connection.getHeaderField("Cache-Control") != null) + response.addHeader("Cache-Control", connection.getHeaderField("Cache-Control")); + return response; + } else { + return serveFile( newURI, header, (AndroidFile) customPath, true ); + } } } return super.serve( uri, method, header, parms, files ); } + + public class InputStreamWithOverloadedClose extends InputStream { + protected InputStream is; + protected HttpURLConnection connection; + + public InputStreamWithOverloadedClose(InputStream is, HttpURLConnection connection) { + super(); + this.is = is; + this.connection = connection; + } + + @Override + public int available() throws IOException { + return is.available(); + } + + @Override + public void close() throws IOException { + is.close(); + connection.disconnect(); + } + + @Override + public void mark(int readlimit) { + is.mark(readlimit); + } + + @Override + public boolean markSupported() { + return is.markSupported(); + } + + @Override + public int read() throws IOException { + return is.read(); + } + + @Override + public int read(byte[] buffer) throws IOException { + return is.read(buffer); + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + return is.read(buffer, offset, length); + } + + @Override + public synchronized void reset() throws IOException { + is.reset(); + } + + @Override + public long skip(long byteCount) throws IOException { + return is.skip(byteCount); + } + } } diff --git a/src/ios/CorHttpd.m b/src/ios/CorHttpd.m index 633315d..fe0b908 100644 --- a/src/ios/CorHttpd.m +++ b/src/ios/CorHttpd.m @@ -158,7 +158,7 @@ - (void)startServer:(CDVInvokedUrlCommand*)command NSString* path = (NSString*) obj; NSString* localPath = nil; const char * docroot = [path UTF8String]; - if(*docroot == '/') { + if(*docroot == '/' || [path hasPrefix:@"http://"] || [path hasPrefix:@"https://"]) { localPath = path; } else { NSString* basePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"www"]; diff --git a/src/ios/CustomPathHTTPConnection.h b/src/ios/CustomPathHTTPConnection.h index 978143c..4f5159b 100644 --- a/src/ios/CustomPathHTTPConnection.h +++ b/src/ios/CustomPathHTTPConnection.h @@ -1,7 +1,17 @@ #import "HTTPConnection.h" +#import "HTTPDataResponse.h" @interface CustomPathHTTPConnection : HTTPConnection + (NSDictionary *) customPaths; + (void) setCustomPaths:(NSDictionary *) cusPaths; - (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory documentRoot:(NSString *) documentRoot; +- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path; +@end + +@interface HTTPDataResponseWithHeaders : HTTPDataResponse +{ + NSDictionary * headers; +} +- (id)initWithDataAndHeaders:(NSData *)data httpHeaders:(NSDictionary *) httpHeaders; +- (NSDictionary *)httpHeaders; @end \ No newline at end of file diff --git a/src/ios/CustomPathHTTPConnection.m b/src/ios/CustomPathHTTPConnection.m index b92c84b..3a94335 100644 --- a/src/ios/CustomPathHTTPConnection.m +++ b/src/ios/CustomPathHTTPConnection.m @@ -1,12 +1,10 @@ #import +#import "CustomPathHTTPConnection.h" #import "HTTPConnection.h" #import "HTTPLogging.h" - -@interface CustomPathHTTPConnection : HTTPConnection -+ (NSDictionary *) customPaths; -+ (void) setCustomPaths:(NSDictionary *) cusPaths; -- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory documentRoot:(NSString *) documentRoot; -@end +#import "HTTPResponse.h" +#import "HTTPErrorResponse.h" +#import "HTTPDataResponse.h" @implementation CustomPathHTTPConnection : HTTPConnection static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; @@ -14,6 +12,59 @@ @implementation CustomPathHTTPConnection : HTTPConnection + (NSDictionary *) customPaths { @synchronized(self) { return customPaths; } } + (void) setCustomPaths:(NSDictionary *) cusPaths { @synchronized(self) { customPaths = cusPaths; } } +/** + * This method is called to get a response for a request. + * You may return any object that adopts the HTTPResponse protocol. + * The HTTPServer comes with two such classes: HTTPFileResponse and HTTPDataResponse. + * HTTPFileResponse is a wrapper for an NSFileHandle object, and is the preferred way to send a file response. + * HTTPDataResponse is a wrapper for an NSData object, and may be used to send a custom response. + **/ +- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path +{ + HTTPLogTrace(); + + __block NSObject * httpResponseReturnable = nil; + + /* + * The following block of code determins if the URL you have requested should be backended to the named + * URL in the custom path. If it does, we'll make a synchronous request for it, and generate a + * HTTPDataResponse object and pass that back. If the backend throws any errors, we'll generate a + * HTTPErrorRepsonse object with the status code, and pass that back. If none of these match, we'll + * allow the existing code to happen. + */ + [CustomPathHTTPConnection.customPaths enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) + { + NSString* customPath = (NSString*) key; + if ([path hasPrefix:customPath] && ([obj hasPrefix:@"http://"] || [obj hasPrefix:@"https://"])) { + *stop = YES; + NSString* subPath = [path substringFromIndex:[customPath length]]; + NSURL *realURL = [NSURL URLWithString: [(NSString *) obj stringByAppendingString: subPath]]; + // turn it into a request and use NSData to load its content + NSURLRequest *urlRequest = [NSURLRequest requestWithURL:realURL]; + NSHTTPURLResponse * urlResponse = nil; + NSError * error = nil; + NSData * data = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&urlResponse error:&error]; + + if (error != nil) { + httpResponseReturnable = [[HTTPErrorResponse alloc] initWithErrorCode: (int) urlResponse.statusCode]; + } else { + httpResponseReturnable = [[HTTPDataResponseWithHeaders alloc] initWithDataAndHeaders: data httpHeaders:urlResponse.allHeaderFields]; + } + } + }]; + + if (httpResponseReturnable != nil) + { + return httpResponseReturnable; + } + else + { + return [super httpResponseForMethod:path URI:path]; + } + +} + + - (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory { HTTPLogTrace(); @@ -125,4 +176,22 @@ - (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirector return fullPath; } +@end + +@implementation HTTPDataResponseWithHeaders : HTTPDataResponse +- (id)initWithDataAndHeaders:(NSData *)dataToUse httpHeaders:(NSDictionary *) httpHeaders +{ + if((self = [super initWithData: dataToUse])) + { + headers = [NSMutableDictionary dictionaryWithDictionary:httpHeaders]; + [(NSMutableDictionary *) headers removeObjectForKey:@"Content-Encoding"]; + [(NSMutableDictionary *) headers removeObjectForKey:@"Content-Length"]; + } + return self; +} + +- (NSDictionary *)httpHeaders +{ + return headers; +} @end \ No newline at end of file From 8388c5b570c8932ad62891389b67e395637fdbce Mon Sep 17 00:00:00 2001 From: davyboyhayes Date: Tue, 9 Feb 2016 15:07:47 +0000 Subject: [PATCH 15/15] Allowing for custom paths to be stacked from different locations (so /local/ can point to a local file based URL, but /local/game/ can point to a HTTP reference) --- src/android/WebServer.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/android/WebServer.java b/src/android/WebServer.java index a53158c..405409f 100644 --- a/src/android/WebServer.java +++ b/src/android/WebServer.java @@ -9,6 +9,8 @@ import java.net.InetSocketAddress; import java.net.HttpURLConnection; import java.net.URL; +import java.util.Arrays; +import java.util.Comparator; import java.util.Iterator; import java.util.Map; import java.util.Properties; @@ -31,7 +33,7 @@ public WebServer(int port, AndroidFile wwwroot, Map customPaths ) throws IOExcep super(port, wwwroot); addCustomPaths(customPaths); } - + private void addCustomPaths(Map customPaths) { this.customPaths = customPaths; customURIs = new String[customPaths.keySet().size()]; @@ -41,7 +43,15 @@ private void addCustomPaths(Map customPaths) { String path = (String) keys.next(); customURIs[i] = path; i++; - Log.i( LOGTAG, "Custom Path: " + path); + } + Arrays.sort(customURIs, new Comparator() { + @Override + public int compare(String s, String t1) { + return t1.length() - s.length(); + } + }); + for (i = 0; i < customURIs.length; i++) { + Log.i( LOGTAG, "Custom Path: " + customURIs[i]); } }