| /* |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| */ |
| |
| #import "CDVLocalWebServer.h" |
| #import "GCDWebServerPrivate.h" |
| #import <Cordova/CDVViewController.h> |
| #import <Cordova/NSDictionary+CordovaPreferences.h> |
| #import <AssetsLibrary/AssetsLibrary.h> |
| #import <MobileCoreServices/MobileCoreServices.h> |
| #import <objc/message.h> |
| #import <netinet/in.h> |
| |
| |
| #define LOCAL_FILESYSTEM_PATH @"local-filesystem" |
| #define ASSETS_LIBRARY_PATH @"assets-library" |
| #define ERROR_PATH @"error" |
| |
| @interface GCDWebServer() |
| - (GCDWebServerResponse*)_responseWithContentsOfDirectory:(NSString*)path; |
| @end |
| |
| |
| @implementation CDVLocalWebServer |
| |
| - (void) pluginInitialize { |
| |
| BOOL useLocalWebServer = NO; |
| BOOL requirementsOK = NO; |
| NSString* indexPage = @"index.html"; |
| NSString* appBasePath = @"www"; |
| NSUInteger port = 80; |
| |
| // check the content tag src |
| CDVViewController* vc = (CDVViewController*)self.viewController; |
| NSURL* startPageUrl = [NSURL URLWithString:vc.startPage]; |
| if (startPageUrl != nil) { |
| if ([[startPageUrl scheme] isEqualToString:@"http"] && [[startPageUrl host] isEqualToString:@"localhost"]) { |
| port = [[startPageUrl port] unsignedIntegerValue]; |
| useLocalWebServer = YES; |
| } |
| } |
| |
| requirementsOK = [self checkRequirements]; |
| if (!requirementsOK) { |
| useLocalWebServer = NO; |
| NSString* alternateContentSrc = [self.commandDelegate.settings cordovaSettingForKey:@"AlternateContentSrc"]; |
| vc.startPage = alternateContentSrc? alternateContentSrc : indexPage; |
| } |
| |
| // check setting |
| #if TARGET_IPHONE_SIMULATOR |
| if (useLocalWebServer) { |
| NSNumber* startOnSimulatorSetting = [[self.commandDelegate settings] objectForKey:[@"CordovaLocalWebServerStartOnSimulator" lowercaseString]]; |
| if (startOnSimulatorSetting) { |
| useLocalWebServer = [startOnSimulatorSetting boolValue]; |
| } |
| } |
| #endif |
| |
| if (port == 0) { |
| // CB-9096 - actually test for an available port, and set it explicitly |
| port = [self _availablePort]; |
| } |
| |
| NSString* authToken = [NSString stringWithFormat:@"cdvToken=%@", [[NSProcessInfo processInfo] globallyUniqueString]]; |
| |
| self.server = [[GCDWebServer alloc] init]; |
| [GCDWebServer setLogLevel:kGCDWebServerLoggingLevel_Error]; |
| |
| if (useLocalWebServer) { |
| [self addAppFileSystemHandler:authToken basePath:[NSString stringWithFormat:@"/%@/", appBasePath] indexPage:indexPage]; |
| |
| // add after server is started to get the true port |
| [self addFileSystemHandlers:authToken]; |
| [self addErrorSystemHandler:authToken]; |
| |
| // handlers must be added before server starts |
| [self.server startWithPort:port bonjourName:nil]; |
| |
| // Update the startPage (supported in cordova-ios 3.7.0, see https://issues.apache.org/jira/browse/CB-7857) |
| vc.startPage = [NSString stringWithFormat:@"http://localhost:%lu/%@/%@?%@", (unsigned long)self.server.port, appBasePath, indexPage, authToken]; |
| |
| } else { |
| if (requirementsOK) { |
| NSString* error = [NSString stringWithFormat:@"WARNING: CordovaLocalWebServer: <content> tag src is not http://localhost[:port] (is %@).", vc.startPage]; |
| NSLog(@"%@", error); |
| |
| [self addErrorSystemHandler:authToken]; |
| |
| // handlers must be added before server starts |
| [self.server startWithPort:port bonjourName:nil]; |
| |
| vc.startPage = [self createErrorUrl:error authToken:authToken]; |
| } else { |
| GWS_LOG_ERROR(@"%@ stopped, failed requirements check.", [self.server class]); |
| } |
| } |
| } |
| |
| - (NSUInteger) _availablePort |
| { |
| struct sockaddr_in addr4; |
| bzero(&addr4, sizeof(addr4)); |
| addr4.sin_len = sizeof(addr4); |
| addr4.sin_family = AF_INET; |
| addr4.sin_port = 0; // set to 0 and bind to find available port |
| addr4.sin_addr.s_addr = htonl(INADDR_ANY); |
| |
| int listeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); |
| if (bind(listeningSocket, (const void*)&addr4, sizeof(addr4)) == 0) { |
| struct sockaddr addr; |
| socklen_t addrlen = sizeof(addr); |
| if (getsockname(listeningSocket, &addr, &addrlen) == 0) { |
| struct sockaddr_in* sockaddr = (struct sockaddr_in*)&addr; |
| close(listeningSocket); |
| return ntohs(sockaddr->sin_port); |
| } |
| } |
| |
| return 0; |
| } |
| |
| - (BOOL) checkRequirements |
| { |
| NSString* pluginName = @"CDVWKWebViewEngine"; |
| |
| BOOL hasWkWebView = NSClassFromString(@"WKWebView") != nil; |
| BOOL wkEnginePlugin = [[self.commandDelegate.settings cordovaSettingForKey:@"CordovaWebViewEngine"] isEqualToString:pluginName]; |
| |
| if (!hasWkWebView) { |
| NSLog(@"[ERROR] %@: WKWebView class not found in the current runtime version.", [self class]); |
| } |
| |
| if (!wkEnginePlugin) { |
| NSLog(@"[ERROR] %@: CordovaWebViewEngine preference must be %@", [self class], pluginName); |
| } |
| |
| return hasWkWebView && wkEnginePlugin; |
| } |
| |
| - (NSString*) createErrorUrl:(NSString*)error authToken:(NSString*)authToken |
| { |
| return [NSString stringWithFormat:@"http://localhost:%lu/%@/%@?%@", (unsigned long)self.server.port, ERROR_PATH, [error stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding], authToken]; |
| } |
| |
| - (void) addFileSystemHandlers:(NSString*)authToken |
| { |
| [self addLocalFileSystemHandler:authToken]; |
| [self addAssetLibraryFileSystemHandler:authToken]; |
| |
| SEL sel = NSSelectorFromString(@"setUrlTransformer:"); |
| __weak __typeof(self) weakSelf = self; |
| |
| if ([self.commandDelegate respondsToSelector:sel]) { |
| NSURL* (^urlTransformer)(NSURL*) = ^NSURL* (NSURL* urlToTransform) { |
| NSURL* localServerURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%lu", (unsigned long)weakSelf.server.port]]; |
| |
| NSURL* transformedUrl = urlToTransform; |
| |
| NSString* localhostUrlString = [NSString stringWithFormat:@"http://localhost:%lu", (unsigned long)[localServerURL.port unsignedIntegerValue]]; |
| |
| if ([[urlToTransform scheme] isEqualToString:ASSETS_LIBRARY_PATH]) { |
| transformedUrl = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@/%@%@", |
| localhostUrlString, |
| ASSETS_LIBRARY_PATH, |
| urlToTransform.host, |
| urlToTransform.path |
| ]]; |
| |
| } else if ([[urlToTransform scheme] isEqualToString:@"file"]) { |
| transformedUrl = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@%@", |
| localhostUrlString, |
| LOCAL_FILESYSTEM_PATH, |
| urlToTransform.path |
| ]]; |
| } |
| |
| return transformedUrl; |
| }; |
| |
| ((void (*)(id, SEL, id))objc_msgSend)(self.commandDelegate, sel, urlTransformer); |
| |
| } else { |
| NSLog(@"WARNING: CDVPlugin's commandDelegate is missing a urlTransformer property. The local web server can't set it to transform file and asset-library urls"); |
| } |
| } |
| |
| - (void) addFileSystemHandler:(GCDWebServerAsyncProcessBlock)processRequestForResponseBlock basePath:(NSString*)basePath authToken:(NSString*)authToken cacheAge:(NSUInteger)cacheAge |
| { |
| GCDWebServerMatchBlock matchBlock = ^GCDWebServerRequest *(NSString* requestMethod, NSURL* requestURL, NSDictionary* requestHeaders, NSString* urlPath, NSDictionary* urlQuery) { |
| |
| if (![requestMethod isEqualToString:@"GET"]) { |
| return nil; |
| } |
| if (![urlPath hasPrefix:basePath]) { |
| return nil; |
| } |
| return [[GCDWebServerRequest alloc] initWithMethod:requestMethod url:requestURL headers:requestHeaders path:urlPath query:urlQuery]; |
| }; |
| |
| GCDWebServerAsyncProcessBlock asyncProcessBlock = ^void (GCDWebServerRequest* request, GCDWebServerCompletionBlock complete) { |
| |
| //check if it is a request from localhost |
| NSString *host = [request.headers objectForKey:@"Host"]; |
| if (host==nil || [host hasPrefix:@"localhost"] == NO ) { |
| complete([GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"FORBIDDEN"]); |
| return; |
| } |
| |
| //check if the querystring or the cookie has the token |
| BOOL hasToken = (request.URL.query && [request.URL.query containsString:authToken]); |
| NSString *cookie = [request.headers objectForKey:@"Cookie"]; |
| BOOL hasCookie = (cookie && [cookie containsString:authToken]); |
| if (!hasToken && !hasCookie) { |
| complete([GCDWebServerErrorResponse responseWithClientError:kGCDWebServerHTTPStatusCode_Forbidden message:@"FORBIDDEN"]); |
| return; |
| } |
| |
| processRequestForResponseBlock(request, ^void(GCDWebServerResponse* response){ |
| if (response) { |
| response.cacheControlMaxAge = cacheAge; |
| } else { |
| response = [GCDWebServerResponse responseWithStatusCode:kGCDWebServerHTTPStatusCode_NotFound]; |
| } |
| |
| if (hasToken && !hasCookie) { |
| //set cookie |
| [response setValue:[NSString stringWithFormat:@"%@;path=/", authToken] forAdditionalHeader:@"Set-Cookie"]; |
| } |
| complete(response); |
| }); |
| }; |
| |
| [self.server addHandlerWithMatchBlock:matchBlock asyncProcessBlock:asyncProcessBlock]; |
| } |
| |
| - (void) addAppFileSystemHandler:(NSString*)authToken basePath:(NSString*)basePath indexPage:(NSString*)indexPage |
| { |
| BOOL allowRangeRequests = YES; |
| |
| NSString* directoryPath = [[self.commandDelegate pathForResource:indexPage] stringByDeletingLastPathComponent]; |
| ; |
| |
| GCDWebServerAsyncProcessBlock processRequestBlock = ^void (GCDWebServerRequest* request, GCDWebServerCompletionBlock complete) { |
| |
| NSString* filePath = [directoryPath stringByAppendingPathComponent:[request.path substringFromIndex:basePath.length]]; |
| NSString* fileType = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL] fileType]; |
| GCDWebServerResponse* response = nil; |
| |
| if (fileType) { |
| if ([fileType isEqualToString:NSFileTypeDirectory]) { |
| if (indexPage) { |
| NSString* indexPath = [filePath stringByAppendingPathComponent:indexPage]; |
| NSString* indexType = [[[NSFileManager defaultManager] attributesOfItemAtPath:indexPath error:NULL] fileType]; |
| if ([indexType isEqualToString:NSFileTypeRegular]) { |
| complete([GCDWebServerFileResponse responseWithFile:indexPath]); |
| } |
| } |
| response = [self.server _responseWithContentsOfDirectory:filePath]; |
| } else if ([fileType isEqualToString:NSFileTypeRegular]) { |
| if (allowRangeRequests) { |
| response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange]; |
| [response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"]; |
| } else { |
| response = [GCDWebServerFileResponse responseWithFile:filePath]; |
| } |
| } |
| } |
| |
| complete(response); |
| }; |
| |
| [self addFileSystemHandler:processRequestBlock basePath:basePath authToken:authToken cacheAge:0]; |
| } |
| |
| - (void) addLocalFileSystemHandler:(NSString*)authToken |
| { |
| NSString* basePath = [NSString stringWithFormat:@"/%@/", LOCAL_FILESYSTEM_PATH]; |
| BOOL allowRangeRequests = YES; |
| |
| GCDWebServerAsyncProcessBlock processRequestBlock = ^void (GCDWebServerRequest* request, GCDWebServerCompletionBlock complete) { |
| |
| NSString* filePath = [request.path substringFromIndex:basePath.length]; |
| NSString* fileType = [[[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:NULL] fileType]; |
| GCDWebServerResponse* response = nil; |
| |
| if (fileType && [fileType isEqualToString:NSFileTypeRegular]) { |
| if (allowRangeRequests) { |
| response = [GCDWebServerFileResponse responseWithFile:filePath byteRange:request.byteRange]; |
| [response setValue:@"bytes" forAdditionalHeader:@"Accept-Ranges"]; |
| } else { |
| response = [GCDWebServerFileResponse responseWithFile:filePath]; |
| } |
| } |
| |
| complete(response); |
| }; |
| |
| [self addFileSystemHandler:processRequestBlock basePath:basePath authToken:authToken cacheAge:0]; |
| } |
| |
| - (void) addAssetLibraryFileSystemHandler:(NSString*)authToken |
| { |
| NSString* basePath = [NSString stringWithFormat:@"/%@/", ASSETS_LIBRARY_PATH]; |
| |
| GCDWebServerAsyncProcessBlock processRequestBlock = ^void (GCDWebServerRequest* request, GCDWebServerCompletionBlock complete) { |
| |
| NSURL* assetUrl = [NSURL URLWithString:[NSString stringWithFormat:@"assets-library:/%@", [request.path substringFromIndex:basePath.length]]]; |
| |
| ALAssetsLibrary* assetsLibrary = [[ALAssetsLibrary alloc] init]; |
| [assetsLibrary assetForURL:assetUrl |
| resultBlock:^(ALAsset* asset) { |
| if (asset) { |
| // We have the asset! Get the data and send it off. |
| ALAssetRepresentation* assetRepresentation = [asset defaultRepresentation]; |
| Byte* buffer = (Byte*)malloc([assetRepresentation size]); |
| NSUInteger bufferSize = [assetRepresentation getBytes:buffer fromOffset:0.0 length:[assetRepresentation size] error:nil]; |
| NSData* data = [NSData dataWithBytesNoCopy:buffer length:bufferSize freeWhenDone:YES]; |
| NSString* MIMEType = (__bridge_transfer NSString*)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)[assetRepresentation UTI], kUTTagClassMIMEType); |
| |
| complete([GCDWebServerDataResponse responseWithData:data contentType:MIMEType]); |
| } else { |
| complete(nil); |
| } |
| } |
| failureBlock:^(NSError* error) { |
| NSLog(@"Error: %@", error); |
| complete(nil); |
| } |
| ]; |
| }; |
| |
| [self addFileSystemHandler:processRequestBlock basePath:basePath authToken:authToken cacheAge:0]; |
| } |
| |
| - (void) addErrorSystemHandler:(NSString*)authToken |
| { |
| NSString* basePath = [NSString stringWithFormat:@"/%@/", ERROR_PATH]; |
| |
| GCDWebServerAsyncProcessBlock processRequestBlock = ^void (GCDWebServerRequest* request, GCDWebServerCompletionBlock complete) { |
| |
| NSString* errorString = [request.path substringFromIndex:basePath.length]; // error string is from the url path |
| NSString* html = [NSString stringWithFormat:@"<h1 style='margin-top:40px; font-size:6vw'>ERROR</h1><h2 style='font-size:3vw'>%@</h2>", errorString]; |
| GCDWebServerResponse* response = [GCDWebServerDataResponse responseWithHTML:html]; |
| complete(response); |
| }; |
| |
| [self addFileSystemHandler:processRequestBlock basePath:basePath authToken:authToken cacheAge:0]; |
| } |
| |
| |
| @end |