From 6374b877a36d1998da5fc9880217bd15f2c2d390 Mon Sep 17 00:00:00 2001 From: Niklas Merz Date: Tue, 1 Oct 2019 19:27:43 +0200 Subject: [PATCH] feat(ios): add http(s) proxy/scheme This adds a new URLSchemeHandler which can proxy http(s) requests to external servers. This is useful for some CORS issues and webview bugs that affect the use of cookies in CORS requests via XHR and fetch. For that reason cookies will be synced between proxied requests on the native layer and the webview. --- src/ios/IONAssetHandler.h | 1 + src/ios/IONAssetHandler.m | 113 +++++++++++++++++++++++++++++--------- src/www/util.js | 6 ++ 3 files changed, 94 insertions(+), 26 deletions(-) diff --git a/src/ios/IONAssetHandler.h b/src/ios/IONAssetHandler.h index aee8d1c7..d0d897cb 100644 --- a/src/ios/IONAssetHandler.h +++ b/src/ios/IONAssetHandler.h @@ -5,6 +5,7 @@ @property (nonatomic, strong) NSString * basePath; @property (nonatomic, strong) NSString * scheme; +@property (nonatomic) Boolean isRunning; -(void)setAssetPath:(NSString *)assetPath; - (instancetype)initWithBasePath:(NSString *)basePath andScheme:(NSString *)scheme; diff --git a/src/ios/IONAssetHandler.m b/src/ios/IONAssetHandler.m index 5ed1f0f8..cc21d7ee 100644 --- a/src/ios/IONAssetHandler.m +++ b/src/ios/IONAssetHandler.m @@ -19,14 +19,72 @@ - (instancetype)initWithBasePath:(NSString *)basePath andScheme:(NSString *)sche - (void)webView:(WKWebView *)webView startURLSchemeTask:(id )urlSchemeTask { + self.isRunning = true; + Boolean loadFile = true; NSString * startPath = @""; NSURL * url = urlSchemeTask.request.URL; - NSString * stringToLoad = url.path; + NSDictionary * header = urlSchemeTask.request.allHTTPHeaderFields; + NSMutableString * stringToLoad = [NSMutableString string]; + [stringToLoad appendString:url.path]; NSString * scheme = url.scheme; + NSString * method = urlSchemeTask.request.HTTPMethod; + NSData * body = urlSchemeTask.request.HTTPBody; + NSData * data; + NSInteger statusCode; if ([scheme isEqualToString:self.scheme]) { if ([stringToLoad hasPrefix:@"/_app_file_"]) { startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_app_file_" withString:@""]; + } else if ([stringToLoad hasPrefix:@"/_http_proxy_"]||[stringToLoad hasPrefix:@"/_https_proxy_"]) { + if(url.query) { + [stringToLoad appendString:@"?"]; + [stringToLoad appendString:url.query]; + } + loadFile = false; + startPath = [stringToLoad stringByReplacingOccurrencesOfString:@"/_http_proxy_" withString:@"http://"]; + startPath = [startPath stringByReplacingOccurrencesOfString:@"/_https_proxy_" withString:@"https://"]; + NSURL * requestUrl = [NSURL URLWithString:startPath]; + WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore]; + WKHTTPCookieStore* cookieStore = dataStore.httpCookieStore; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + [request setHTTPMethod:method]; + [request setURL:requestUrl]; + if (body) { + [request setHTTPBody:body]; + } + [request setAllHTTPHeaderFields:header]; + [request setHTTPShouldHandleCookies:YES]; + + [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if(error) { + NSLog(@"Proxy error: %@", error); + } + + // set cookies to WKWebView + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; + if(httpResponse) { + NSArray* cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[httpResponse allHeaderFields] forURL:response.URL]; + [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies forURL:httpResponse.URL mainDocumentURL:nil]; + cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; + + for (NSHTTPCookie* c in cookies) + { + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){ + //running in background thread is necessary because setCookie otherwise fails + dispatch_async(dispatch_get_main_queue(), ^(void){ + [cookieStore setCookie:c completionHandler:nil]; + }); + }); + }; + } + + // Do not use urlSchemeTask if it has been closed in stopURLSchemeTask + if(self.isRunning) { + [urlSchemeTask didReceiveResponse:response]; + [urlSchemeTask didReceiveData:data]; + [urlSchemeTask didFinish]; + } + }] resume]; } else { startPath = self.basePath; if ([stringToLoad isEqualToString:@""] || [url.pathExtension isEqualToString:@""]) { @@ -36,36 +94,39 @@ - (void)webView:(WKWebView *)webView startURLSchemeTask:(id )ur } } } - NSError * fileError = nil; - NSData * data = nil; - if ([self isMediaExtension:url.pathExtension]) { - data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError]; - } - if (!data || fileError) { - data = [[NSData alloc] initWithContentsOfFile:startPath]; - } - NSInteger statusCode = 200; - if (!data) { - statusCode = 404; - } - NSURL * localUrl = [NSURL URLWithString:url.absoluteString]; - NSString * mimeType = [self getMimeType:url.pathExtension]; - id response = nil; - if (data && [self isMediaExtension:url.pathExtension]) { - response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil]; - } else { - NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"}; - response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers]; - } - - [urlSchemeTask didReceiveResponse:response]; - [urlSchemeTask didReceiveData:data]; - [urlSchemeTask didFinish]; + if(loadFile) { + NSError * fileError = nil; + data = nil; + if ([self isMediaExtension:url.pathExtension]) { + data = [NSData dataWithContentsOfFile:startPath options:NSDataReadingMappedIfSafe error:&fileError]; + } + if (!data || fileError) { + data = [[NSData alloc] initWithContentsOfFile:startPath]; + } + statusCode = 200; + if (!data) { + statusCode = 404; + } + NSURL * localUrl = [NSURL URLWithString:url.absoluteString]; + NSString * mimeType = [self getMimeType:url.pathExtension]; + id response = nil; + if (data && [self isMediaExtension:url.pathExtension]) { + response = [[NSURLResponse alloc] initWithURL:localUrl MIMEType:mimeType expectedContentLength:data.length textEncodingName:nil]; + } else { + NSDictionary * headers = @{ @"Content-Type" : mimeType, @"Cache-Control": @"no-cache"}; + response = [[NSHTTPURLResponse alloc] initWithURL:localUrl statusCode:statusCode HTTPVersion:nil headerFields:headers]; + } + + [urlSchemeTask didReceiveResponse:response]; + [urlSchemeTask didReceiveData:data]; + [urlSchemeTask didFinish]; + } } - (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id)urlSchemeTask { + self.isRunning = false; NSLog(@"stop"); } diff --git a/src/www/util.js b/src/www/util.js index 4acf8851..7d66d0a8 100644 --- a/src/www/util.js +++ b/src/www/util.js @@ -11,6 +11,12 @@ var WebView = { if (url.startsWith('file://')) { return window.WEBVIEW_SERVER_URL + url.replace('file://', '/_app_file_'); } + if (url.startsWith('http://')) { + return window.WEBVIEW_SERVER_URL + url.replace('http://', '/_http_proxy_'); + } + if (url.startsWith('https://')) { + return window.WEBVIEW_SERVER_URL + url.replace('https://', '/_https_proxy_'); + } if (url.startsWith('content://')) { return window.WEBVIEW_SERVER_URL + url.replace('content:/', '/_app_content_'); }