diff --git a/src/objc/ModalWebViewController.h b/src/objc/ModalWebViewController.h index b9d42fc..0bb6d58 100644 --- a/src/objc/ModalWebViewController.h +++ b/src/objc/ModalWebViewController.h @@ -8,7 +8,8 @@ - (instancetype)initWithRequest:(NSMutableURLRequest *)request userAgent:(NSString *)userAgent - interceptRequests:(bool)interceptRequests; + interceptRequests:(bool)interceptRequests + initialCookies:(NSArray *)initialCookies; - (void)close; - (void)openRequest:(NSMutableURLRequest *)request; - (NSDictionary *)getBrowserCookiesForCurrentUrl; diff --git a/src/objc/ModalWebViewController.mm b/src/objc/ModalWebViewController.mm index 33600a7..e63e36a 100644 --- a/src/objc/ModalWebViewController.mm +++ b/src/objc/ModalWebViewController.mm @@ -10,6 +10,7 @@ @interface ModalWebViewController () @property (nonatomic, strong) NSMutableArray *visitedUrls; @property (nonatomic, assign) NSInteger eventCounter; @property (nonatomic, assign) bool interceptRequests; +@property (nonatomic, strong) NSArray *initialCookies; @end @implementation ModalWebViewController @@ -36,87 +37,90 @@ - (void)viewDidLoad { if (self.interceptRequests) { NSString *injectedJS = @"(function() {\n" - " const log = (requestType, data) => {\n" - " try {\n" - " window.webkit.messageHandlers.interceptedRequestHandler.postMessage({ requestType, data });\n" - " } catch (e) {}\n" + " const handler = window.webkit.messageHandlers.interceptedRequestHandler;\n" + " const log = (requestType, data) => { try { handler.postMessage({ requestType, data }); } catch(e) {} };\n" + " \n" + " const nativeToString = Function.prototype.toString;\n" + " const nativeCallToString = Function.prototype.call.bind(nativeToString);\n" + " const wrappedFns = new WeakMap();\n" + " \n" + " Function.prototype.toString = function() {\n" + " if (wrappedFns.has(this)) {\n" + " return wrappedFns.get(this);\n" + " }\n" + " return nativeCallToString(this);\n" " };\n" + " wrappedFns.set(Function.prototype.toString, 'function toString() { [native code] }');\n" + " \n" " const originalFetch = window.fetch;\n" - " window.fetch = async function(input, init) {\n" - " try {\n" - " const method = (init && init.method) || (typeof input === \"string\" ? \"GET\" : input.method || \"GET\");\n" - " const url = typeof input === \"string\" ? input : input.url;\n" - " let requestHeaders = init?.headers || {};\n" - " if (requestHeaders instanceof Headers) {\n" - " requestHeaders = Object.fromEntries(requestHeaders.entries());\n" - " }\n" - " log(\"fetch_request\", {\n" - " url,\n" - " method,\n" - " headers: requestHeaders,\n" - " body: init?.body,\n" - " });\n" - " const response = await originalFetch.apply(this, arguments);\n" - " const clonedResponse = response.clone();\n" - " let responseHeaders = clonedResponse.headers || {};\n" - " if (responseHeaders instanceof Headers) {\n" - " responseHeaders = Object.fromEntries(responseHeaders.entries());\n" - " }\n" - " log(\"fetch_response\", {\n" - " url,\n" - " method,\n" - " headers: responseHeaders,\n" - " body: await clonedResponse.text(),\n" - " status: clonedResponse.status,\n" + " const wrappedFetch = function fetch(input, init) {\n" + " const method = (init && init.method) || (typeof input === 'string' ? 'GET' : input.method || 'GET');\n" + " const url = typeof input === 'string' ? input : input.url;\n" + " let requestHeaders = init?.headers || {};\n" + " if (requestHeaders instanceof Headers) requestHeaders = Object.fromEntries(requestHeaders.entries());\n" + " log('fetch_request', { url, method, headers: requestHeaders, body: init?.body });\n" + " return originalFetch.apply(this, arguments).then(function(response) {\n" + " const cloned = response.clone();\n" + " let responseHeaders = cloned.headers || {};\n" + " if (responseHeaders instanceof Headers) responseHeaders = Object.fromEntries(responseHeaders.entries());\n" + " cloned.text().then(function(body) {\n" + " log('fetch_response', { url, method, headers: responseHeaders, body, status: cloned.status });\n" " });\n" " return response;\n" - " } catch (err) {\n" - " log(\"fetch_error\", err.toString());\n" - " throw err;\n" - " }\n" + " });\n" " };\n" + " wrappedFns.set(wrappedFetch, 'function fetch() { [native code] }');\n" + " Object.defineProperty(window, 'fetch', { value: wrappedFetch, writable: true, configurable: true });\n" + " \n" " const OriginalXHR = window.XMLHttpRequest;\n" - "\n" - " function PatchedXHR() {\n" - " const xhr = new OriginalXHR();\n" - " let _method = \"\";\n" - " let _url = \"\";\n" - " let headers = {};\n" - " xhr.open = new Proxy(xhr.open, {\n" - " apply(t, thisArg, args) {\n" - " _method = args[0];\n" - " _url = args[1];\n" - " return Reflect.apply(t, thisArg, args);\n" - " },\n" - " });\n" - " const setRequestHeader = xhr.setRequestHeader;\n" - " xhr.setRequestHeader = function(name, value) {\n" - " headers[name] = value;\n" - " return setRequestHeader.apply(xhr, arguments);\n" - " };\n" - " xhr.send = new Proxy(xhr.send, {\n" - " apply(t, thisArg, args) {\n" - " log(\"xhr_request\", {\n" - " method: _method,\n" - " url: _url,\n" - " headers,\n" - " body: args[0],\n" - " });\n" - " xhr.addEventListener(\"loadend\", function() {\n" - " log(\"xhr_response\", {\n" - " method: _method,\n" - " url: _url,\n" - " headers,\n" - " body: xhr.responseText || xhr.response,\n" - " status: xhr.status,\n" - " });\n" - " });\n" - " return Reflect.apply(t, thisArg, args);\n" - " },\n" - " });\n" - " return xhr;\n" - " }\n" - " window.XMLHttpRequest = PatchedXHR;\n" + " const xhrProto = OriginalXHR.prototype;\n" + " const originalOpen = xhrProto.open;\n" + " const originalSend = xhrProto.send;\n" + " const originalSetHeader = xhrProto.setRequestHeader;\n" + " const xhrData = new WeakMap();\n" + " \n" + " xhrProto.open = function(method, url) {\n" + " xhrData.set(this, { method, url, headers: {} });\n" + " return originalOpen.apply(this, arguments);\n" + " };\n" + " wrappedFns.set(xhrProto.open, 'function open() { [native code] }');\n" + " \n" + " xhrProto.setRequestHeader = function(name, value) {\n" + " const data = xhrData.get(this);\n" + " if (data) data.headers[name] = value;\n" + " return originalSetHeader.apply(this, arguments);\n" + " };\n" + " wrappedFns.set(xhrProto.setRequestHeader, 'function setRequestHeader() { [native code] }');\n" + " \n" + " xhrProto.send = function(body) {\n" + " const data = xhrData.get(this);\n" + " if (data) {\n" + " log('xhr_request', { method: data.method, url: data.url, headers: data.headers, body });\n" + " this.addEventListener('loadend', () => {\n" + " log('xhr_response', { method: data.method, url: data.url, headers: data.headers, body: this.responseText || this.response, status: this.status });\n" + " });\n" + " }\n" + " return originalSend.apply(this, arguments);\n" + " };\n" + " wrappedFns.set(xhrProto.send, 'function send() { [native code] }');\n" + " \n" + " Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true });\n" + " \n" + " const automationProps = ['__webdriver_script_fn', '__driver_evaluate', '__webdriver_evaluate',\n" + " '__selenium_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', '__webdriver_unwrapped',\n" + " '__selenium_unwrapped', '__fxdriver_unwrapped', '_Selenium_IDE_Recorder', '_selenium',\n" + " 'calledSelenium', '_WEBDRIVER_ELEM_CACHE', 'ChromeDriverw', 'driver-hierarchical',\n" + " '__nightmare', '__phantomas', '_phantom', 'phantom', 'callPhantom'];\n" + " automationProps.forEach(p => { try { Object.defineProperty(window, p, { get: () => undefined, configurable: true }); } catch(e) {} });\n" + " \n" + " const OriginalError = Error;\n" + " Error = function(...args) {\n" + " const err = new OriginalError(...args);\n" + " if (err.stack) err.stack = err.stack.replace(/\\n.*interceptedRequestHandler.*/g, '');\n" + " return err;\n" + " };\n" + " Error.prototype = OriginalError.prototype;\n" + " Object.setPrototypeOf(Error, OriginalError);\n" "})();\n"; @@ -154,9 +158,21 @@ - (void)viewDidLoad { self.webView.customUserAgent = self.customUserAgent; } - // Load the provided URL + // Load the provided URL, injecting any initial cookies first if (self.request) { - [self.webView loadRequest:self.request]; + if (self.initialCookies.count > 0) { + WKHTTPCookieStore *cookieStore = self.websiteDataStore.httpCookieStore; + dispatch_group_t group = dispatch_group_create(); + for (NSHTTPCookie *cookie in self.initialCookies) { + dispatch_group_enter(group); + [cookieStore setCookie:cookie completionHandler:^{ dispatch_group_leave(group); }]; + } + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ + [self.webView loadRequest:self.request]; + }); + } else { + [self.webView loadRequest:self.request]; + } } } @@ -327,13 +343,15 @@ - (void)openRequest:(NSMutableURLRequest *)request { - (instancetype)initWithRequest:(NSMutableURLRequest *)request userAgent:(NSString *)userAgent - interceptRequests:(bool)interceptRequests { + interceptRequests:(bool)interceptRequests + initialCookies:(NSArray *)initialCookies { self = [super init]; if (self) { _request = request; _customUserAgent = userAgent; _interceptRequests = interceptRequests; + _initialCookies = initialCookies; } return self; diff --git a/src/objc/OpacityObjCWrapper.mm b/src/objc/OpacityObjCWrapper.mm index 8925493..0b91ed9 100644 --- a/src/objc/OpacityObjCWrapper.mm +++ b/src/objc/OpacityObjCWrapper.mm @@ -62,7 +62,7 @@ + (int)initialize:(NSString *)api_key } opacity_core::register_ios_callbacks( - ios_prepare_request, ios_set_request_header, ios_present_webview, + ios_prepare_request, ios_set_request_header, ios_set_cookie, ios_present_webview, ios_close_webview, ios_get_browser_cookies_for_current_url, ios_get_browser_cookies_for_domain, get_ip_address, get_battery_level, get_battery_status, get_carrier_name, get_carrier_mcc, get_carrier_mnc, diff --git a/src/objc/helper_functions.h b/src/objc/helper_functions.h index 6e58b0e..7275e87 100644 --- a/src/objc/helper_functions.h +++ b/src/objc/helper_functions.h @@ -6,6 +6,7 @@ extern "C" // Webview functions void ios_prepare_request(const char *url); void ios_set_request_header(const char *key, const char *value); + void ios_set_cookie(const char *url, const char *value); void ios_present_webview(bool interceptRequests); void ios_close_webview(); const char *ios_get_browser_cookies_for_domain(const char *domain); diff --git a/src/objc/helper_functions.mm b/src/objc/helper_functions.mm index cefd81a..e3a49f4 100644 --- a/src/objc/helper_functions.mm +++ b/src/objc/helper_functions.mm @@ -13,6 +13,7 @@ NSMutableURLRequest *request; UINavigationController *navController; NSString *userAgent; +NSMutableArray *pendingCookies; UIViewController *topMostViewController() { // Fetch the key window's root view controller @@ -31,6 +32,23 @@ void ios_prepare_request(const char *url) { NSString *urlString = [NSString stringWithUTF8String:url]; request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]]; + pendingCookies = [NSMutableArray array]; +} + +void ios_set_cookie(const char *url_c, const char *value_c) { + NSString *urlString = [NSString stringWithUTF8String:url_c]; + NSString *cookieString = [NSString stringWithUTF8String:value_c]; + NSURL *url = [NSURL URLWithString:urlString]; + if (!url || !cookieString) { + return; + } + NSArray *cookies = + [NSHTTPCookie cookiesWithResponseHeaderFields:@{@"Set-Cookie": cookieString} + forURL:url]; + if (!pendingCookies) { + pendingCookies = [NSMutableArray array]; + } + [pendingCookies addObjectsFromArray:cookies]; } void ios_set_request_header(const char *key, const char *value) { @@ -56,9 +74,13 @@ void ios_present_webview(bool intercept_requests) { @"dismissed"); } + NSArray *cookiesToInject = [pendingCookies copy]; + pendingCookies = [NSMutableArray array]; + modalWebVC = [[ModalWebViewController alloc] initWithRequest:request userAgent:userAgent - interceptRequests:intercept_requests]; + interceptRequests:intercept_requests + initialCookies:cookiesToInject]; // Set an on dismiss callback modalWebVC.onDismissCallback = ^{ diff --git a/src/objc/sdk.h b/src/objc/sdk.h index 716e189..9cf4e50 100644 --- a/src/objc/sdk.h +++ b/src/objc/sdk.h @@ -18,6 +18,8 @@ typedef void (*IosPrepareRequestFn)(const char*); typedef void (*IosSetRequestHeaderFn)(const char*, const char*); +typedef void (*IosSetCookieFn)(const char*, const char*); + typedef void (*IosPresentWebviewFn)(bool); typedef void (*IosCloseWebviewFn)(void); @@ -208,6 +210,7 @@ extern const char *get_ip_address(void); void register_ios_callbacks(IosPrepareRequestFn ios_prepare_request, IosSetRequestHeaderFn ios_set_request_header, + IosSetCookieFn ios_set_cookie, IosPresentWebviewFn ios_present_webview, IosCloseWebviewFn ios_close_webview, IosGetBrowserCookiesForCurrentUrlFn ios_get_browser_cookies_for_current_url, @@ -250,7 +253,7 @@ extern void android_prepare_request(const char *url); extern void android_set_request_header(const char *key, const char *value); -extern void android_present_webview(bool should_intercept); +extern void android_present_webview(bool should_intercept, bool android_use_system_webview); extern void android_close_webview(void);