From 88355df244a2549babb5c63f4992553d63b00a59 Mon Sep 17 00:00:00 2001 From: Roman Rosluk Date: Wed, 28 Nov 2018 14:58:47 +0100 Subject: [PATCH 1/8] feat(CorsProxy): Implementation web proxy based on local server in order to avoid CORS errors --- src/ios/CDVWKCorsProxy.h | 201 +++++++++++++++++++++++++ src/ios/CDVWKCorsProxy.swift | 160 ++++++++++++++++++++ src/ios/GCDWebServer-Bridging-Header.h | 3 + 3 files changed, 364 insertions(+) create mode 100644 src/ios/CDVWKCorsProxy.h create mode 100644 src/ios/CDVWKCorsProxy.swift create mode 100644 src/ios/GCDWebServer-Bridging-Header.h diff --git a/src/ios/CDVWKCorsProxy.h b/src/ios/CDVWKCorsProxy.h new file mode 100644 index 00000000..ecd1590a --- /dev/null +++ b/src/ios/CDVWKCorsProxy.h @@ -0,0 +1,201 @@ +// Generated by Apple Swift version 4.2.1 effective-3.4.1 (swiftlang-1000.11.42 clang-1000.11.45.1) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" + +#if !defined(__has_include) +# define __has_include(x) 0 +#endif +#if !defined(__has_attribute) +# define __has_attribute(x) 0 +#endif +#if !defined(__has_feature) +# define __has_feature(x) 0 +#endif +#if !defined(__has_warning) +# define __has_warning(x) 0 +#endif + +#if __has_include() +# include +#endif + +#pragma clang diagnostic ignored "-Wauto-import" +#include +#include +#include +#include + +#if !defined(SWIFT_TYPEDEFS) +# define SWIFT_TYPEDEFS 1 +# if __has_include() +# include +# elif !defined(__cplusplus) +typedef uint_least16_t char16_t; +typedef uint_least32_t char32_t; +# endif +typedef float swift_float2 __attribute__((__ext_vector_type__(2))); +typedef float swift_float3 __attribute__((__ext_vector_type__(3))); +typedef float swift_float4 __attribute__((__ext_vector_type__(4))); +typedef double swift_double2 __attribute__((__ext_vector_type__(2))); +typedef double swift_double3 __attribute__((__ext_vector_type__(3))); +typedef double swift_double4 __attribute__((__ext_vector_type__(4))); +typedef int swift_int2 __attribute__((__ext_vector_type__(2))); +typedef int swift_int3 __attribute__((__ext_vector_type__(3))); +typedef int swift_int4 __attribute__((__ext_vector_type__(4))); +typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2))); +typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3))); +typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); +#endif + +#if !defined(SWIFT_PASTE) +# define SWIFT_PASTE_HELPER(x, y) x##y +# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y) +#endif +#if !defined(SWIFT_METATYPE) +# define SWIFT_METATYPE(X) Class +#endif +#if !defined(SWIFT_CLASS_PROPERTY) +# if __has_feature(objc_class_property) +# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__ +# else +# define SWIFT_CLASS_PROPERTY(...) +# endif +#endif + +#if __has_attribute(objc_runtime_name) +# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X))) +#else +# define SWIFT_RUNTIME_NAME(X) +#endif +#if __has_attribute(swift_name) +# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X))) +#else +# define SWIFT_COMPILE_NAME(X) +#endif +#if __has_attribute(objc_method_family) +# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X))) +#else +# define SWIFT_METHOD_FAMILY(X) +#endif +#if __has_attribute(noescape) +# define SWIFT_NOESCAPE __attribute__((noescape)) +#else +# define SWIFT_NOESCAPE +#endif +#if __has_attribute(warn_unused_result) +# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +#else +# define SWIFT_WARN_UNUSED_RESULT +#endif +#if __has_attribute(noreturn) +# define SWIFT_NORETURN __attribute__((noreturn)) +#else +# define SWIFT_NORETURN +#endif +#if !defined(SWIFT_CLASS_EXTRA) +# define SWIFT_CLASS_EXTRA +#endif +#if !defined(SWIFT_PROTOCOL_EXTRA) +# define SWIFT_PROTOCOL_EXTRA +#endif +#if !defined(SWIFT_ENUM_EXTRA) +# define SWIFT_ENUM_EXTRA +#endif +#if !defined(SWIFT_CLASS) +# if __has_attribute(objc_subclassing_restricted) +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# else +# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA +# endif +#endif + +#if !defined(SWIFT_PROTOCOL) +# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA +#endif + +#if !defined(SWIFT_EXTENSION) +# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__) +#endif + +#if !defined(OBJC_DESIGNATED_INITIALIZER) +# if __has_attribute(objc_designated_initializer) +# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) +# else +# define OBJC_DESIGNATED_INITIALIZER +# endif +#endif +#if !defined(SWIFT_ENUM_ATTR) +# if defined(__has_attribute) && __has_attribute(enum_extensibility) +# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility))) +# else +# define SWIFT_ENUM_ATTR(_extensibility) +# endif +#endif +#if !defined(SWIFT_ENUM) +# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# if __has_feature(generalized_swift_name) +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type +# else +# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility) +# endif +#endif +#if !defined(SWIFT_UNAVAILABLE) +# define SWIFT_UNAVAILABLE __attribute__((unavailable)) +#endif +#if !defined(SWIFT_UNAVAILABLE_MSG) +# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg))) +#endif +#if !defined(SWIFT_AVAILABILITY) +# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__))) +#endif +#if !defined(SWIFT_DEPRECATED) +# define SWIFT_DEPRECATED __attribute__((deprecated)) +#endif +#if !defined(SWIFT_DEPRECATED_MSG) +# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__))) +#endif +#if __has_feature(attribute_diagnose_if_objc) +# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning"))) +#else +# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg) +#endif +#if __has_feature(modules) +@import Foundation; +@import ObjectiveC; +#endif + +#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" +#pragma clang diagnostic ignored "-Wduplicate-method-arg" +#if __has_warning("-Wpragma-clang-attribute") +# pragma clang diagnostic ignored "-Wpragma-clang-attribute" +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wnullability" + +#if __has_attribute(external_source_symbol) +# pragma push_macro("any") +# undef any +# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="ionicCors",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol)) +# pragma pop_macro("any") +#endif + +@class GCDWebServer; +@class NSXMLParser; + +SWIFT_CLASS("_TtC9ionicCors14CDVWKCorsProxy") +@interface CDVWKCorsProxy : NSObject +@property (nonatomic, strong) GCDWebServer * _Nonnull webserver; +- (nonnull instancetype)initWithWebserver:(GCDWebServer * _Nonnull)webserver OBJC_DESIGNATED_INITIALIZER; +- (void)setHandlersWithUrlPrefix:(NSString * _Nonnull)urlPrefix serverUrl:(NSString * _Nonnull)serverUrl; +- (void)parser:(NSXMLParser * _Nonnull)parser didStartElement:(NSString * _Nonnull)elementName namespaceURI:(NSString * _Nullable)namespaceURI qualifiedName:(NSString * _Nullable)qName attributes:(NSDictionary * _Nonnull)attributeDict; +- (nonnull instancetype)init SWIFT_UNAVAILABLE; ++ (nonnull instancetype)new SWIFT_DEPRECATED_MSG("-init is unavailable"); +@end + +#if __has_attribute(external_source_symbol) +# pragma clang attribute pop +#endif +#pragma clang diagnostic pop diff --git a/src/ios/CDVWKCorsProxy.swift b/src/ios/CDVWKCorsProxy.swift new file mode 100644 index 00000000..9dbfc514 --- /dev/null +++ b/src/ios/CDVWKCorsProxy.swift @@ -0,0 +1,160 @@ +// +// CDVWKCorsProxy.swift +// +// +// Created by Raman Rasliuk on 27.11.18. +// + +import Foundation + +@objc class CDVWKCorsProxy : NSObject, XMLParserDelegate { + + var webserver: GCDWebServer + + private var skipHeaders = ["content-encoding", "content-security-policy"] + + init(webserver: GCDWebServer) { + self.webserver = webserver + + super.init() + + self.getConfig(); + + } + + func setHandlers(urlPrefix: String, serverUrl: String) { + + let pattern = "^" + NSRegularExpression.escapedPattern(for: urlPrefix) + ".*" + + webserver.addHandler(forMethod: "GET", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock: { req in + return self.sendProxyResult(urlPrefix, serverUrl, req) + }) + + webserver.addHandler(forMethod: "POST", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock:{ req in + return self.sendProxyResult(urlPrefix, serverUrl, req) + }) + + webserver.addHandler(forMethod: "PUT", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock:{ req in + return self.sendProxyResult(urlPrefix, serverUrl, req) + }) + + webserver.addHandler(forMethod: "PATCH", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock:{ req in + return self.sendProxyResult(urlPrefix, serverUrl, req) + }) + + webserver.addHandler(forMethod: "DELETE", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock:{ req in + return self.sendProxyResult(urlPrefix, serverUrl, req) + }) + + } + + private func sendProxyResult(_ prefix: String, _ serverUrl: String, _ req: GCDWebServerRequest) -> GCDWebServerResponse? { + + let query = req.url.query == nil ? "" : "?" + req.url.query! + let url = URL(string: serverUrl + req.path.substring(from: prefix.endIndex) + query) + + if (url == nil) { + return self.sendError(error: "Invalid url") + } + + let request = NSMutableURLRequest(url: url!, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 320000) + + request.httpMethod = req.method + request.allHTTPHeaderFields = req.headers as? [String: String] + request.allHTTPHeaderFields?["Host"] = url!.host + + if (req.hasBody()) { + request.httpBody = (req as! GCDWebServerDataRequest).data + } + + var finalResponse: GCDWebServerDataResponse? = nil + + let session = URLSession.shared + + let task = session.dataTask(with: request as URLRequest) { data, urlResp, error in + if (error != nil) { + finalResponse = self.sendError(error: error?.localizedDescription) as? GCDWebServerDataResponse + + return + } + + let httpResponse = urlResp as! HTTPURLResponse + + let resp = GCDWebServerDataResponse(data: data!, contentType: "application/x-unknown") + + resp.statusCode = httpResponse.statusCode + + for key in httpResponse.allHeaderFields { + + let headerKey: String! = self.toString(v: key.0 as AnyObject) + let headerValue: String! = self.toString(v: key.1 as AnyObject) + + let headerKeyLower = headerKey.lowercased() + + if (headerKey == "" || self.skipHeaders.contains(headerKeyLower)) { + continue + } + + resp.setValue(headerValue, forAdditionalHeader: headerKey) + + } + + resp.setValue(String(data!.count), forAdditionalHeader: "Content-Length") + + finalResponse = resp + + } + + task.resume() + + while (finalResponse == nil) { + Thread.sleep(forTimeInterval: 0.001) + } + + return finalResponse + + } + + private func getConfig() { + if let path = Bundle.main.url(forResource: "config", withExtension: "xml") { + if let parser = XMLParser(contentsOf: path) { + parser.delegate = self + parser.parse() + } + } + } + + private func sendError(error: String?) -> GCDWebServerResponse! { + let msg = error == nil ? "An error occured" : error! + let errorData = msg.data(using: String.Encoding.utf8, allowLossyConversion: true) + let resp = GCDWebServerDataResponse(data: errorData!, contentType: "text/plain") + resp.statusCode = 500 + + return resp + } + + private func toString(v: AnyObject?) -> String! { + if (v == nil) { return ""; } + return String(stringInterpolationSegment: v!) + } + + + func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) { + if elementName == "wkproxy" { + + let path = attributeDict["path"] + let proxyUrl = attributeDict["proxyUrl"] + + if (path != nil && proxyUrl != nil) { + + print("Setting proxy path", path!, "to address", proxyUrl!) + + self.setHandlers(urlPrefix: path!, serverUrl: proxyUrl!) + } + + } + } + +} + + diff --git a/src/ios/GCDWebServer-Bridging-Header.h b/src/ios/GCDWebServer-Bridging-Header.h new file mode 100644 index 00000000..cb9b7806 --- /dev/null +++ b/src/ios/GCDWebServer-Bridging-Header.h @@ -0,0 +1,3 @@ +#import "GCDWebServer.h" +#import "GCDWebServerDataResponse.h" +#import "GCDWebServerDataRequest.h" From 9a4a9557f9843ecbfde97aae42600834e651844f Mon Sep 17 00:00:00 2001 From: Roman Rosluk Date: Wed, 28 Nov 2018 15:01:55 +0100 Subject: [PATCH 2/8] feat(CorsProxy): Implementation web proxy based on local server in order to avoid CORS errors --- README.md | 10 ++++++++++ plugin.xml | 5 +++++ src/ios/CDVWKWebViewEngine.m | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/README.md b/README.md index 6e4bc269..c269ea66 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,16 @@ the plugin now restricts access to only the app itself. Whether to use a dark styled keyboard on iOS +#### Proxy requests to avoid CORS errors + +```xml + +``` + +All requests which starts with `/api/` will be forwarder to proxyUrl +(eg. https://www.domain.com/api/) + + ## Plugin Requirements * **iOS**: iOS 10+ and `cordova-ios` 4+ diff --git a/plugin.xml b/plugin.xml index d606bd4c..ce380a81 100644 --- a/plugin.xml +++ b/plugin.xml @@ -36,6 +36,8 @@ + + @@ -76,6 +78,9 @@ + + + diff --git a/src/ios/CDVWKWebViewEngine.m b/src/ios/CDVWKWebViewEngine.m index a0e27f78..4e0cc0d4 100644 --- a/src/ios/CDVWKWebViewEngine.m +++ b/src/ios/CDVWKWebViewEngine.m @@ -28,6 +28,7 @@ Licensed to the Apache Software Foundation (ASF) under one #import "CDVWKProcessPoolFactory.h" #import "GCDWebServer.h" #import "GCDWebServerPrivate.h" +#import "CDVWKCorsProxy.h" #define CDV_BRIDGE_NAME @"cordova" #define CDV_IONIC_STOP_SCROLL @"stopScroll" @@ -104,6 +105,7 @@ @interface CDVWKWebViewEngine () @property (nonatomic, strong, readwrite) id uiDelegate; @property (nonatomic, weak) id weakScriptMessageHandler; @property (nonatomic, strong) GCDWebServer *webServer; +@property (nonatomic, strong) CDVWKCorsProxy *corsProxy; @property (nonatomic, readwrite) CGRect frame; @property (nonatomic, strong) NSString *userAgentCreds; @property (nonatomic, assign) BOOL internalConnectionsOnly; @@ -166,6 +168,8 @@ - (void)initWebServer [self updateBindPath]; [self setServerPath:wwwPath]; + self.corsProxy = [[CDVWKCorsProxy alloc] initWithWebserver: self.webServer]; + [self startServer]; } From 222d18cce202901048b722a5340f28832f063e16 Mon Sep 17 00:00:00 2001 From: Roman Rosluk Date: Thu, 29 Nov 2018 12:25:11 +0100 Subject: [PATCH 3/8] feat(CorsProxy): Proxy ssl pinning --- README.md | 26 ++++++- src/ios/CDVWKCorsProxy.swift | 135 +++++++++++++++++++++++++++++------ 2 files changed, 138 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index c269ea66..2f4220ca 100644 --- a/README.md +++ b/README.md @@ -94,12 +94,36 @@ Whether to use a dark styled keyboard on iOS #### Proxy requests to avoid CORS errors ```xml - + ``` All requests which starts with `/api/` will be forwarder to proxyUrl (eg. https://www.domain.com/api/) +* `path` - path which will be proxied (all starts with that path) +* `proxyUrl` - where to forward +* `sslCheck` - (optional) mode of ssl `nocheck`, `default`, `pinned` +* `useCertificates` - (optional) comma separated `.der` files to use for the pining + +##### Proxy SSL Pining + +There are 3 modes which can be set for the sslCheck + +* `default` - (used by default). Check will be by iOS +* `nocheck` - will accept all certificates, even self-signed +* `pinned` - will accept connections only with provided DER certificates + +The certificate files should be provided in the `www/certificates` directory + +It is also possible to explicitly specify which certificates will be used by providing +there's names (comma separated) in the `useCertificates` attribute of `wkproxy`. If the `useCertificates` +is not provided, it will use all `.der` files. + +How to get DER certificate from my server? + +`openssl s_client -connect my-server.com:443 -showcerts < /dev/null | openssl x509 -outform DER > mycert.der` + + ## Plugin Requirements diff --git a/src/ios/CDVWKCorsProxy.swift b/src/ios/CDVWKCorsProxy.swift index 9dbfc514..8b3b3150 100644 --- a/src/ios/CDVWKCorsProxy.swift +++ b/src/ios/CDVWKCorsProxy.swift @@ -22,33 +22,37 @@ import Foundation } - func setHandlers(urlPrefix: String, serverUrl: String) { + func setHandlers(urlPrefix: String?, serverUrl: String?, sslCheck: String?, useCertificates: [String]?) { - let pattern = "^" + NSRegularExpression.escapedPattern(for: urlPrefix) + ".*" + if (urlPrefix == nil || serverUrl == nil) { + print("ERROR SETTING PROXY. missing path or proxyUrl") - webserver.addHandler(forMethod: "GET", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock: { req in - return self.sendProxyResult(urlPrefix, serverUrl, req) - }) + return + } + + let pattern = "^" + NSRegularExpression.escapedPattern(for: urlPrefix!) + ".*" + + let sslCheck = sslCheck ?? "default" - webserver.addHandler(forMethod: "POST", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock:{ req in - return self.sendProxyResult(urlPrefix, serverUrl, req) - }) + print("Setting proxy path", urlPrefix!, "to address", serverUrl!, "with ssl check mode", sslCheck) - webserver.addHandler(forMethod: "PUT", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock:{ req in - return self.sendProxyResult(urlPrefix, serverUrl, req) - }) + var sslTrust: URLSessionDelegate? - webserver.addHandler(forMethod: "PATCH", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock:{ req in - return self.sendProxyResult(urlPrefix, serverUrl, req) - }) + if (sslCheck == "nocheck") { + sslTrust = SSLTrustAny() + } else if (sslCheck == "pinned") { + sslTrust = SSLPinned(useCertificates) + } - webserver.addHandler(forMethod: "DELETE", pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock:{ req in - return self.sendProxyResult(urlPrefix, serverUrl, req) - }) + for method in ["GET", "POST", "PUT", "PATCH", "DELETE"] { + webserver.addHandler(forMethod: method, pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock: { req in + return self.sendProxyResult(urlPrefix!, serverUrl!, req, sslTrust) + }) + } } - private func sendProxyResult(_ prefix: String, _ serverUrl: String, _ req: GCDWebServerRequest) -> GCDWebServerResponse? { + private func sendProxyResult(_ prefix: String, _ serverUrl: String, _ req: GCDWebServerRequest, _ sslTrust: URLSessionDelegate?) -> GCDWebServerResponse? { let query = req.url.query == nil ? "" : "?" + req.url.query! let url = URL(string: serverUrl + req.path.substring(from: prefix.endIndex) + query) @@ -68,8 +72,15 @@ import Foundation } var finalResponse: GCDWebServerDataResponse? = nil + var session: URLSession! + + if (sslTrust != nil) { + let configuration = URLSessionConfiguration.default + session = URLSession(configuration: configuration, delegate: sslTrust, delegateQueue: OperationQueue.main) + } else { + session = URLSession.shared + } - let session = URLSession.shared let task = session.dataTask(with: request as URLRequest) { data, urlResp, error in if (error != nil) { @@ -144,17 +155,97 @@ import Foundation let path = attributeDict["path"] let proxyUrl = attributeDict["proxyUrl"] + let sslCheck = attributeDict["sslCheck"] + let useCertificates = attributeDict["useCertificates"] if (path != nil && proxyUrl != nil) { + self.setHandlers(urlPrefix: path!, serverUrl: proxyUrl!, sslCheck: sslCheck, useCertificates: useCertificates?.components(separatedBy: ",")) + } - print("Setting proxy path", path!, "to address", proxyUrl!) + } + } - self.setHandlers(urlPrefix: path!, serverUrl: proxyUrl!) - } +} + +class SSLTrustAny : NSObject, URLSessionDelegate { + + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + + if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) { + completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) + + return } + + completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) } } +class SSLPinned : NSObject, URLSessionDelegate { + + private var certificates: [Data] = [] + + init(_ useCertificates: [String]?) { + super.init() + + let certsPath = URL(fileURLWithPath: Bundle.main.bundlePath + "/www/certificates", isDirectory: true) + + let fileManager = FileManager.default + + if let enumerator = fileManager.enumerator(atPath: certsPath.path) { + for file in enumerator { + let certFileName = file as! String + + let fileUrl = URL(fileURLWithPath: certFileName, relativeTo: certsPath) + + if (fileUrl.path.hasSuffix(".der") && (useCertificates == nil || useCertificates!.contains(certFileName))) { + do { + let certData = try Data(contentsOf: fileUrl) + self.certificates.append(certData) + } catch { + print("ERROR to load certificate", fileUrl.path) + } + } + + } + } + + } + + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) { + + // Adapted from OWASP https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning#iOS + + if (certificates.count > 0 && challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) { + if let serverTrust = challenge.protectionSpace.serverTrust { + var secresult = SecTrustResultType.invalid + let status = SecTrustEvaluate(serverTrust, &secresult) + + if(errSecSuccess == status) { + if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { + let serverCertificateData = SecCertificateCopyData(serverCertificate) + let data = CFDataGetBytePtr(serverCertificateData); + let size = CFDataGetLength(serverCertificateData); + let certServer = Data(bytes: data!, count: size) + + for certData in self.certificates { + if (certServer == certData) { + completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust)) + + return + } + } + + } + } + } + } + + // Pinning failed + completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) + } + +} From c4f7901f9b2ea829dd216c77799198a5b5c6bfc0 Mon Sep 17 00:00:00 2001 From: Roman Rosluk Date: Thu, 29 Nov 2018 16:08:13 +0100 Subject: [PATCH 4/8] feat(CorsProxy): Proxy ssl clear cookies on start --- README.md | 8 +++++++- src/ios/CDVWKCorsProxy.swift | 35 ++++++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 2f4220ca..6d5ae22d 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Whether to use a dark styled keyboard on iOS #### Proxy requests to avoid CORS errors ```xml - + ``` All requests which starts with `/api/` will be forwarder to proxyUrl @@ -104,6 +104,7 @@ All requests which starts with `/api/` will be forwarder to proxyUrl * `proxyUrl` - where to forward * `sslCheck` - (optional) mode of ssl `nocheck`, `default`, `pinned` * `useCertificates` - (optional) comma separated `.der` files to use for the pining +* `clearCookies` - (optional) default `no`. If `yes` all cookies will be removed on app start ##### Proxy SSL Pining @@ -124,6 +125,11 @@ How to get DER certificate from my server? `openssl s_client -connect my-server.com:443 -showcerts < /dev/null | openssl x509 -outform DER > mycert.der` +##### Cookies + +Cookies are stored on the native system and are not forwarded to the WKWebView. + + ## Plugin Requirements diff --git a/src/ios/CDVWKCorsProxy.swift b/src/ios/CDVWKCorsProxy.swift index 8b3b3150..a747cdfc 100644 --- a/src/ios/CDVWKCorsProxy.swift +++ b/src/ios/CDVWKCorsProxy.swift @@ -11,7 +11,7 @@ import Foundation var webserver: GCDWebServer - private var skipHeaders = ["content-encoding", "content-security-policy"] + private var skipHeaders = ["content-encoding", "content-security-policy", "set-cookie"] init(webserver: GCDWebServer) { self.webserver = webserver @@ -20,12 +20,13 @@ import Foundation self.getConfig(); + } - func setHandlers(urlPrefix: String?, serverUrl: String?, sslCheck: String?, useCertificates: [String]?) { + func setHandlers(urlPrefix: String?, serverUrl: String?, sslCheck: String?, useCertificates: [String]?, clearCookies: String?) { if (urlPrefix == nil || serverUrl == nil) { - print("ERROR SETTING PROXY. missing path or proxyUrl") + print("WK PROXY: ERROR SETTING PROXY. missing path or proxyUrl") return } @@ -34,7 +35,7 @@ import Foundation let sslCheck = sslCheck ?? "default" - print("Setting proxy path", urlPrefix!, "to address", serverUrl!, "with ssl check mode", sslCheck) + print("WK PROXY: Setting proxy path", urlPrefix!, "to address", serverUrl!, "with ssl check mode", sslCheck) var sslTrust: URLSessionDelegate? @@ -44,6 +45,20 @@ import Foundation sslTrust = SSLPinned(useCertificates) } + + if (clearCookies == "yes") { + + let cookieStore = HTTPCookieStorage.shared + + print("WK PROXY: Clearing cookies") + + for cookie in cookieStore.cookies ?? [] { + cookieStore.deleteCookie(cookie) + } + + } + + for method in ["GET", "POST", "PUT", "PATCH", "DELETE"] { webserver.addHandler(forMethod: method, pathRegex: pattern, request: GCDWebServerDataRequest.self, processBlock: { req in return self.sendProxyResult(urlPrefix!, serverUrl!, req, sslTrust) @@ -74,11 +89,11 @@ import Foundation var finalResponse: GCDWebServerDataResponse? = nil var session: URLSession! - if (sslTrust != nil) { + if (sslTrust == nil) { + session = URLSession.shared + } else { let configuration = URLSessionConfiguration.default session = URLSession(configuration: configuration, delegate: sslTrust, delegateQueue: OperationQueue.main) - } else { - session = URLSession.shared } @@ -157,9 +172,10 @@ import Foundation let proxyUrl = attributeDict["proxyUrl"] let sslCheck = attributeDict["sslCheck"] let useCertificates = attributeDict["useCertificates"] + let clearCookies = attributeDict["clearCookies"] if (path != nil && proxyUrl != nil) { - self.setHandlers(urlPrefix: path!, serverUrl: proxyUrl!, sslCheck: sslCheck, useCertificates: useCertificates?.components(separatedBy: ",")) + self.setHandlers(urlPrefix: path!, serverUrl: proxyUrl!, sslCheck: sslCheck, useCertificates: useCertificates?.components(separatedBy: ","), clearCookies: clearCookies) } } @@ -206,7 +222,7 @@ class SSLPinned : NSObject, URLSessionDelegate { let certData = try Data(contentsOf: fileUrl) self.certificates.append(certData) } catch { - print("ERROR to load certificate", fileUrl.path) + print("WK PROXY: ERROR to load certificate", fileUrl.path) } } @@ -232,6 +248,7 @@ class SSLPinned : NSObject, URLSessionDelegate { let certServer = Data(bytes: data!, count: size) for certData in self.certificates { + if (certServer == certData) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust)) From c81c9348f6ea46ab99a955914f4282acb7ced928 Mon Sep 17 00:00:00 2001 From: Raman Rasliuk Date: Thu, 29 Nov 2018 16:16:56 +0100 Subject: [PATCH 5/8] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d5ae22d..202ce46d 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ the plugin now restricts access to only the app itself. Whether to use a dark styled keyboard on iOS -#### Proxy requests to avoid CORS errors +#### Proxy requests to avoid CORS errors (iOS only) (BETA) ```xml From c8e39ff6bffaec9fe9379442d5be71e552957a06 Mon Sep 17 00:00:00 2001 From: Roman Rosluk Date: Thu, 29 Nov 2018 18:05:38 +0100 Subject: [PATCH 6/8] Proxy request timeout update --- src/ios/CDVWKCorsProxy.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ios/CDVWKCorsProxy.swift b/src/ios/CDVWKCorsProxy.swift index a747cdfc..efc582d6 100644 --- a/src/ios/CDVWKCorsProxy.swift +++ b/src/ios/CDVWKCorsProxy.swift @@ -76,7 +76,7 @@ import Foundation return self.sendError(error: "Invalid url") } - let request = NSMutableURLRequest(url: url!, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 320000) + let request = NSMutableURLRequest(url: url!, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 120) request.httpMethod = req.method request.allHTTPHeaderFields = req.headers as? [String: String] From 5fed3e5352508ef0084c4910ab43cf6d6ce51729 Mon Sep 17 00:00:00 2001 From: Roman Rosluk Date: Wed, 5 Dec 2018 11:56:35 +0100 Subject: [PATCH 7/8] Added pinning on the intermediate certificate (check in the chain) --- README.md | 24 +++++++++++++++----- src/ios/CDVWKCorsProxy.swift | 43 +++++++++++++++++++++++------------- 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 202ce46d..23909058 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Whether to use a dark styled keyboard on iOS #### Proxy requests to avoid CORS errors (iOS only) (BETA) ```xml - + ``` All requests which starts with `/api/` will be forwarder to proxyUrl @@ -103,7 +103,8 @@ All requests which starts with `/api/` will be forwarder to proxyUrl * `path` - path which will be proxied (all starts with that path) * `proxyUrl` - where to forward * `sslCheck` - (optional) mode of ssl `nocheck`, `default`, `pinned` -* `useCertificates` - (optional) comma separated `.der` files to use for the pining +* `sslCheckChain` - (optional) if "yes" then it will compare all certificates in the chain in order to pin your intermediate certificate +* `useCertificates` - (optional) comma separated `.cer` files to use for the pining * `clearCookies` - (optional) default `no`. If `yes` all cookies will be removed on app start ##### Proxy SSL Pining @@ -112,17 +113,30 @@ There are 3 modes which can be set for the sslCheck * `default` - (used by default). Check will be by iOS * `nocheck` - will accept all certificates, even self-signed -* `pinned` - will accept connections only with provided DER certificates +* `pinned` - will accept connections only with provided DER certificates with `.cer` extension The certificate files should be provided in the `www/certificates` directory It is also possible to explicitly specify which certificates will be used by providing there's names (comma separated) in the `useCertificates` attribute of `wkproxy`. If the `useCertificates` -is not provided, it will use all `.der` files. +is not provided, it will use all `.cer` files. How to get DER certificate from my server? -`openssl s_client -connect my-server.com:443 -showcerts < /dev/null | openssl x509 -outform DER > mycert.der` +```bash +# Leaf certificate +openssl s_client -connect my-server.com:443 -showcerts < /dev/null | openssl x509 -outform DER > mycert.cer +``` + +```bash +# List all certificate chain +openssl s_client -connect my-server.com:443 -showcerts + +# Take (copy) one needed incliding "-----BEGIN CERTIFICATE-----" and "-----END CERTIFICATE-----" +# Create file "certificate.pem" and add all into the file +# Generate DER certificate from PEM with command +openssl x509 -in certificate.pem -outform der -out certificate.cer +``` ##### Cookies diff --git a/src/ios/CDVWKCorsProxy.swift b/src/ios/CDVWKCorsProxy.swift index efc582d6..2d598ce8 100644 --- a/src/ios/CDVWKCorsProxy.swift +++ b/src/ios/CDVWKCorsProxy.swift @@ -23,7 +23,7 @@ import Foundation } - func setHandlers(urlPrefix: String?, serverUrl: String?, sslCheck: String?, useCertificates: [String]?, clearCookies: String?) { + func setHandlers(urlPrefix: String?, serverUrl: String?, sslCheck: String?, useCertificates: [String]?, clearCookies: String?, checkSSLChain: Bool?) { if (urlPrefix == nil || serverUrl == nil) { print("WK PROXY: ERROR SETTING PROXY. missing path or proxyUrl") @@ -42,7 +42,7 @@ import Foundation if (sslCheck == "nocheck") { sslTrust = SSLTrustAny() } else if (sslCheck == "pinned") { - sslTrust = SSLPinned(useCertificates) + sslTrust = SSLPinned(useCertificates, checkSSLChain) } @@ -173,9 +173,10 @@ import Foundation let sslCheck = attributeDict["sslCheck"] let useCertificates = attributeDict["useCertificates"] let clearCookies = attributeDict["clearCookies"] + let checkSSLChain = attributeDict["sslCheckChain"] == "yes" if (path != nil && proxyUrl != nil) { - self.setHandlers(urlPrefix: path!, serverUrl: proxyUrl!, sslCheck: sslCheck, useCertificates: useCertificates?.components(separatedBy: ","), clearCookies: clearCookies) + self.setHandlers(urlPrefix: path!, serverUrl: proxyUrl!, sslCheck: sslCheck, useCertificates: useCertificates?.components(separatedBy: ","), clearCookies: clearCookies, checkSSLChain: checkSSLChain) } } @@ -202,10 +203,15 @@ class SSLTrustAny : NSObject, URLSessionDelegate { class SSLPinned : NSObject, URLSessionDelegate { private var certificates: [Data] = [] + private var checkInCertChain = false - init(_ useCertificates: [String]?) { + init(_ useCertificates: [String]?, _ checkInCertChain: Bool?) { super.init() + if (checkInCertChain != nil) { + self.checkInCertChain = checkInCertChain! + } + let certsPath = URL(fileURLWithPath: Bundle.main.bundlePath + "/www/certificates", isDirectory: true) let fileManager = FileManager.default @@ -217,7 +223,7 @@ class SSLPinned : NSObject, URLSessionDelegate { let fileUrl = URL(fileURLWithPath: certFileName, relativeTo: certsPath) - if (fileUrl.path.hasSuffix(".der") && (useCertificates == nil || useCertificates!.contains(certFileName))) { + if (fileUrl.path.hasSuffix(".cer") && (useCertificates == nil || useCertificates!.contains(certFileName))) { do { let certData = try Data(contentsOf: fileUrl) self.certificates.append(certData) @@ -240,23 +246,30 @@ class SSLPinned : NSObject, URLSessionDelegate { var secresult = SecTrustResultType.invalid let status = SecTrustEvaluate(serverTrust, &secresult) + let count = self.checkInCertChain ? SecTrustGetCertificateCount(serverTrust) : 1 + if(errSecSuccess == status) { - if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { - let serverCertificateData = SecCertificateCopyData(serverCertificate) - let data = CFDataGetBytePtr(serverCertificateData); - let size = CFDataGetLength(serverCertificateData); - let certServer = Data(bytes: data!, count: size) - for certData in self.certificates { + for i in 0.. Date: Wed, 5 Dec 2018 16:42:37 +0100 Subject: [PATCH 8/8] feat (ios): Add URLSchemeHandler for iOS 11+ (#221) * Add URLSchemeHandler for iOS 11+ * Make Scheme usage configurable --- .gitignore | 1 + README.md | 20 ++++++++++ plugin.xml | 3 ++ src/ios/CDVWKWebViewEngine.m | 66 +++++++++++++++++++++++++------- src/ios/IONAssetHandler.h | 10 +++++ src/ios/IONAssetHandler.m | 74 ++++++++++++++++++++++++++++++++++++ 6 files changed, 161 insertions(+), 13 deletions(-) create mode 100644 src/ios/IONAssetHandler.h create mode 100644 src/ios/IONAssetHandler.m diff --git a/.gitignore b/.gitignore index 30b09b4d..8ef01939 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ Thumbs.db node_modules xcuserdata +package-lock.json diff --git a/README.md b/README.md index 6e4bc269..19355761 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,26 @@ The default port the server will listen on. _You should change this to a random Preferences only available for iOS platform +#### UseScheme + +`` + +Default value is `false`. + +On iOS 11 and newer it will use a `WKURLSchemeHandler` that loads the app from `ionic://` scheme instead of using the local web server and `https://` scheme. + +On iOS 10 and older will continue using the local web server even if the preference is set to `true`. + +#### HostName + +`` + +Default value is `app`. + +If `UseScheme` is set to yes, it will use the `HostName` value as the host of the starting url. + +Example `ionic://app` + #### WKSuspendInBackground ```xml diff --git a/plugin.xml b/plugin.xml index d606bd4c..4c6858c8 100644 --- a/plugin.xml +++ b/plugin.xml @@ -62,6 +62,7 @@ + @@ -76,6 +77,8 @@ + + diff --git a/src/ios/CDVWKWebViewEngine.m b/src/ios/CDVWKWebViewEngine.m index a0e27f78..e079f95c 100644 --- a/src/ios/CDVWKWebViewEngine.m +++ b/src/ios/CDVWKWebViewEngine.m @@ -28,6 +28,7 @@ Licensed to the Apache Software Foundation (ASF) under one #import "CDVWKProcessPoolFactory.h" #import "GCDWebServer.h" #import "GCDWebServerPrivate.h" +#import "IONAssetHandler.h" #define CDV_BRIDGE_NAME @"cordova" #define CDV_IONIC_STOP_SCROLL @"stopScroll" @@ -107,6 +108,8 @@ @interface CDVWKWebViewEngine () @property (nonatomic, readwrite) CGRect frame; @property (nonatomic, strong) NSString *userAgentCreds; @property (nonatomic, assign) BOOL internalConnectionsOnly; +@property (nonatomic, assign) BOOL useScheme; +@property (nonatomic, strong) IONAssetHandler * handler; @property (nonatomic, readwrite) NSString *CDV_LOCAL_SERVER; @end @@ -152,6 +155,13 @@ - (void)initWebServer [GCDWebServer setLogLevel: kGCDWebServerLoggingLevel_Warning]; self.webServer = [[GCDWebServer alloc] init]; + [self updateBindPath]; + [self setServerPath:[self getStartPath]]; + + [self startServer]; +} + +-(NSString *) getStartPath { NSString * wwwPath = [[NSBundle mainBundle] pathForResource:@"www" ofType: nil]; NSUserDefaults* userDefaults = [NSUserDefaults standardUserDefaults]; @@ -162,11 +172,8 @@ - (void)initWebServer NSString * snapshots = [cordovaDataDirectory stringByAppendingPathComponent:@"ionic_built_snapshots"]; wwwPath = [snapshots stringByAppendingPathComponent:[persistedPath lastPathComponent]]; } - - [self updateBindPath]; - [self setServerPath:wwwPath]; - - [self startServer]; + self.basePath = wwwPath; + return wwwPath; } -(BOOL) isNewBinary @@ -264,9 +271,22 @@ - (void)pluginInitialize { // viewController would be available now. we attempt to set all possible delegates to it, by default NSDictionary* settings = self.commandDelegate.settings; - self.internalConnectionsOnly = [settings cordovaBoolSettingForKey:@"WKInternalConnectionsOnly" defaultValue:YES]; + if (@available(iOS 11.0, *)) { + self.useScheme = [settings cordovaBoolSettingForKey:@"UseScheme" defaultValue:NO]; + } else { + self.useScheme = NO; + } - [self initWebServer]; + self.internalConnectionsOnly = [settings cordovaBoolSettingForKey:@"WKInternalConnectionsOnly" defaultValue:YES]; + if (self.useScheme) { + NSString *bind = [settings cordovaSettingForKey:@"HostName"]; + if(bind == nil){ + bind = @"app"; + } + self.CDV_LOCAL_SERVER = [NSString stringWithFormat:@"ionic://%@", bind]; + } else { + [self initWebServer]; + } self.uiDelegate = [[CDVWKWebViewUIDelegate alloc] initWithTitle:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]]; @@ -307,6 +327,15 @@ - (void)pluginInitialize WKWebViewConfiguration* configuration = [self createConfigurationFromSettings:settings]; configuration.userContentController = userContentController; + if (@available(iOS 11.0, *)) { + if (self.useScheme) { + self.handler = [[IONAssetHandler alloc] init]; + [self.handler setAssetPath:[self getStartPath]]; + [configuration setURLSchemeHandler:self.handler forURLScheme:@"ionic"]; + [configuration setURLSchemeHandler:self.handler forURLScheme:@"ionic-asset"]; + } + } + // re-create WKWebView, since we need to update configuration // remove from keyWindow before recreating [self.engineWebView removeFromSuperview]; @@ -459,7 +488,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N if (context == KVOContext) { if (object == [self webView] && [keyPath isEqualToString: @"URL"] && [object valueForKeyPath:keyPath] == nil){ NSLog(@"URL is nil. Reloading WKWebView"); - if ([self.webServer isRunning]) { + if ([self isSafeToReload]) { [(WKWebView*)_engineWebView reload]; } else { [self loadErrorPage:nil]; @@ -472,7 +501,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N - (void)onAppWillEnterForeground:(NSNotification *)notification { if ([self shouldReloadWebView]) { - if ([self.webServer isRunning]) { + if ([self isSafeToReload]) { NSLog(@"%@", @"CDVWKWebViewEngine reloading!"); [(WKWebView*)_engineWebView reload]; } else { @@ -516,6 +545,11 @@ - (BOOL)shouldReloadWebView return [self shouldReloadWebView:wkWebView.URL title:wkWebView.title]; } +- (BOOL)isSafeToReload +{ + return [self.webServer isRunning] || self.useScheme; +} + - (BOOL)shouldReloadWebView:(NSURL *)location title:(NSString*)title { BOOL title_is_nil = (title == nil); @@ -551,7 +585,7 @@ - (id)loadRequest:(NSURLRequest *)request } request = [NSURLRequest requestWithURL:url]; } - if ([self.webServer isRunning]) { + if ([self isSafeToReload]) { return [(WKWebView*)_engineWebView loadRequest:request]; } else { return [self loadErrorPage:request]; @@ -831,7 +865,7 @@ - (void)webView:(WKWebView*)theWebView didFailNavigation:(WKNavigation*)navigati - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { - if ([self.webServer isRunning]) { + if ([self isSafeToReload]) { [webView reload]; } else { [self loadErrorPage:nil]; @@ -912,9 +946,15 @@ -(void)getServerBasePath:(CDVInvokedUrlCommand*)command -(void)setServerBasePath:(CDVInvokedUrlCommand*)command { NSString * path = [command argumentAtIndex:0]; - [self setServerPath:path]; + if (self.useScheme) { + self.basePath = path; + [self.handler setAssetPath:path]; + } else { + [self setServerPath:path]; + } + NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.CDV_LOCAL_SERVER]]; - if ([self.webServer isRunning]) { + if ([self isSafeToReload]) { [(WKWebView*)_engineWebView loadRequest:request]; } else { [self loadErrorPage:request]; diff --git a/src/ios/IONAssetHandler.h b/src/ios/IONAssetHandler.h new file mode 100644 index 00000000..61cab670 --- /dev/null +++ b/src/ios/IONAssetHandler.h @@ -0,0 +1,10 @@ +#import +#import + +@interface IONAssetHandler : NSObject + +@property (nonatomic, strong) NSString * basePath; + +-(void)setAssetPath:(NSString *)assetPath; + +@end diff --git a/src/ios/IONAssetHandler.m b/src/ios/IONAssetHandler.m new file mode 100644 index 00000000..f03db6cd --- /dev/null +++ b/src/ios/IONAssetHandler.m @@ -0,0 +1,74 @@ +#import "IONAssetHandler.h" +#import + +@implementation IONAssetHandler + +-(void)setAssetPath:(NSString *)assetPath { + self.basePath = assetPath; +} + +- (void)webView:(WKWebView *)webView startURLSchemeTask:(id )urlSchemeTask +API_AVAILABLE(ios(11.0)){ + NSString * startPath = @""; + NSURL * url = urlSchemeTask.request.URL; + NSString * stringToLoad = url.path; + NSString * scheme = url.scheme; + if ([scheme isEqualToString:@"ionic"]) { + startPath = self.basePath; + if ([stringToLoad isEqualToString:@""] || !url.pathExtension) { + startPath = [startPath stringByAppendingString:@"/index.html"]; + } else { + startPath = [startPath stringByAppendingString:stringToLoad]; + } + } else { + if (![stringToLoad isEqualToString:@""]) { + startPath = stringToLoad; + } + } + + NSData * 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]; + +} + +- (void)webView:(nonnull WKWebView *)webView stopURLSchemeTask:(nonnull id)urlSchemeTask API_AVAILABLE(ios(11.0)){ + NSLog(@"stop"); +} + +-(NSString *) getMimeType:(NSString *)fileExtension { + if (fileExtension && ![fileExtension isEqualToString:@""]) { + NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtension, NULL); + NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType); + return contentType ? contentType : @"application/octet-stream"; + } else { + return @"text/html"; + } +} + +-(BOOL) isMediaExtension:(NSString *) pathExtension { + NSArray * mediaExtensions = @[@"m4v", @"mov", @"mp4", + @"aac", @"ac3", @"aiff", @"au", @"flac", @"m4a", @"mp3", @"wav"]; + if ([mediaExtensions containsObject:pathExtension]) { + return YES; + } + return NO; +} + + +@end