Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/objc/ModalWebViewController.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

- (instancetype)initWithRequest:(NSMutableURLRequest *)request
userAgent:(NSString *)userAgent
interceptRequests:(bool)interceptRequests;
interceptRequests:(bool)interceptRequests
initialCookies:(NSArray<NSHTTPCookie *> *)initialCookies;
- (void)close;
- (void)openRequest:(NSMutableURLRequest *)request;
- (NSDictionary *)getBrowserCookiesForCurrentUrl;
Expand Down
174 changes: 96 additions & 78 deletions src/objc/ModalWebViewController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSHTTPCookie *> *initialCookies;
@end

@implementation ModalWebViewController
Expand All @@ -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"
Comment on lines +43 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahaha, missed seeing this. Overriding the toString, good old times. Fun fact though function toString() { [native code] } is only for chromium based browsers, firefox and safari actually has function () {\n [native code]\n}. But I guess it's not an issue even though we are on WKWebView - so Safari - because they don't yet have that much data from users using WebView

Not really sure if it's worth for you changing it to the one with newlines, but again, you did say adding all of this code made stuff work for Delta when they use akamai, so I guess normal users would probably be detected more 😂 classic antibot behavior

" \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"
Comment on lines +109 to +114
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually interesting, any of these are present in WKWebView? Didn't know

" \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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I knew DD and PX are pretty annoying with CDP error stacks, good catch :P

" return err;\n"
" };\n"
" Error.prototype = OriginalError.prototype;\n"
" Object.setPrototypeOf(Error, OriginalError);\n"
"})();\n";


Expand Down Expand Up @@ -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];
}
}
}

Expand Down Expand Up @@ -327,13 +343,15 @@ - (void)openRequest:(NSMutableURLRequest *)request {

- (instancetype)initWithRequest:(NSMutableURLRequest *)request
userAgent:(NSString *)userAgent
interceptRequests:(bool)interceptRequests {
interceptRequests:(bool)interceptRequests
initialCookies:(NSArray<NSHTTPCookie *> *)initialCookies {
self = [super init];

if (self) {
_request = request;
_customUserAgent = userAgent;
_interceptRequests = interceptRequests;
_initialCookies = initialCookies;
}

return self;
Expand Down
2 changes: 1 addition & 1 deletion src/objc/OpacityObjCWrapper.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/objc/helper_functions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
24 changes: 23 additions & 1 deletion src/objc/helper_functions.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
NSMutableURLRequest *request;
UINavigationController *navController;
NSString *userAgent;
NSMutableArray<NSHTTPCookie *> *pendingCookies;

UIViewController *topMostViewController() {
// Fetch the key window's root view controller
Expand All @@ -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<NSHTTPCookie *> *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) {
Expand All @@ -56,9 +74,13 @@ void ios_present_webview(bool intercept_requests) {
@"dismissed");
}

NSArray<NSHTTPCookie *> *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 = ^{
Expand Down
5 changes: 4 additions & 1 deletion src/objc/sdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
Loading