diff --git a/README.md b/README.md index 19355761..58c6d14b 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,60 @@ 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 (iOS only) (BETA) + +```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` +* `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 + +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 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 `.cer` files. + +How to get DER certificate from my server? + +```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 + +Cookies are stored on the native system and are not forwarded to the WKWebView. + + + ## Plugin Requirements * **iOS**: iOS 10+ and `cordova-ios` 4+ diff --git a/plugin.xml b/plugin.xml index f0b9e13e..5af01c36 100644 --- a/plugin.xml +++ b/plugin.xml @@ -36,6 +36,8 @@ + + @@ -77,6 +79,9 @@ + + + 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..2d598ce8 --- /dev/null +++ b/src/ios/CDVWKCorsProxy.swift @@ -0,0 +1,281 @@ +// +// 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", "set-cookie"] + + init(webserver: GCDWebServer) { + self.webserver = webserver + + super.init() + + self.getConfig(); + + + } + + 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") + + return + } + + let pattern = "^" + NSRegularExpression.escapedPattern(for: urlPrefix!) + ".*" + + let sslCheck = sslCheck ?? "default" + + print("WK PROXY: Setting proxy path", urlPrefix!, "to address", serverUrl!, "with ssl check mode", sslCheck) + + var sslTrust: URLSessionDelegate? + + if (sslCheck == "nocheck") { + sslTrust = SSLTrustAny() + } else if (sslCheck == "pinned") { + sslTrust = SSLPinned(useCertificates, checkSSLChain) + } + + + 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) + }) + } + + } + + 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) + + if (url == nil) { + return self.sendError(error: "Invalid url") + } + + let request = NSMutableURLRequest(url: url!, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 120) + + 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 + var session: URLSession! + + if (sslTrust == nil) { + session = URLSession.shared + } else { + let configuration = URLSessionConfiguration.default + session = URLSession(configuration: configuration, delegate: sslTrust, delegateQueue: OperationQueue.main) + } + + + 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"] + 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, checkSSLChain: checkSSLChain) + } + + } + } + +} + + +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] = [] + private var checkInCertChain = false + + 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 + + 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(".cer") && (useCertificates == nil || useCertificates!.contains(certFileName))) { + do { + let certData = try Data(contentsOf: fileUrl) + self.certificates.append(certData) + } catch { + print("WK PROXY: 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) + + let count = self.checkInCertChain ? SecTrustGetCertificateCount(serverTrust) : 1 + + if(errSecSuccess == status) { + + for i in 0.. 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; @@ -158,6 +160,8 @@ - (void)initWebServer [self updateBindPath]; [self setServerPath:[self getStartPath]]; + self.corsProxy = [[CDVWKCorsProxy alloc] initWithWebserver: self.webServer]; + [self startServer]; } 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"