Intercept responses from WebSocket connection in UIWebView - ios

I have an app which has a UIWebView inside of it with a loaded website. This website has a chart in it which is periodicly updated with data from remote server via websockets (socket.io).
Im new to websockets technology but Im trying to somehow intercept the chart data that the website is receiving from server via it.
Till now I have managed to catch http requests sent by the website of such address format: “http://website-address/socket.io/?auth_token=...”
I have the socket.io library for iOS but don’t know how to use it to somehow spoof the website connection and acquire the data downloaded by the website. Can anyone help? Is it even possible?

Switch to WKWebView if you can. Using javascript bridge is much easier there. That said, with UIWebView, you'd need to inject a script that adds a handler for events received by the socket that you are trying to listen to. You can either create an io variable by yourself but apparently the server needs auth token. If you cannot create an auth token, you can only do this if you have access to the io variable created by the website.
Then for adding a handler, you'll need to know what the event name is, that delivers the chart data. You can snoop around the website and see if you can find that. If you cannot all bets are off. Once we register a handler and get the data, we need to pass this back to your native code. This is where WKWebView would keep it clean by letting you add message handlers that can deliver messages from js to native code. For UIWebView you'll have to create a custom url scheme and spoof a navigation request to pass the data. Lets assume your custom url scheme is 'myApp'. Then the script you'd need to inject would be:
<script>
/* if you can access/create the auth token
var socket = io('http://website-address/socket.io/?auth_token=');
*/
var socket = getioReferenceCreatedByWebsite();
socket.on('<eventName>',function(){
window.location = 'myApp://<data>';
};
</script>
In your native code:
...
webView.delegate = self;
[webView stringByEvaluatingJavaScriptFromString:#"<theAboveJSAsAString>"];
....
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
if(request.URL.scheme == #"myApp"){
NSString *data = request.URL.path;
//handle the data
return NO;
}
return YES;
}

In regards to Santhosh R answer. I had the problem he mentioned where I could not get a reference to the websocket object as it was caught up in a closure.
I solved this by adding in a preload script which wraps the native Websocket object to store any instantiated websocket objects in an array and then return the newly created websocket object.
Here is the code.
in your WebView element add in a preload attribute.
<webview id="myWebview" src="http://exmple.com" preload="./interceptor.js"></webview>
and then in inteceptor.js
window.NativeWebsocket = WebSocket;
window.WebSocket = function(url, protocols){
window.interceptedWebsockets = [];
var ws = new NativeWebsocket(url, protocols);
interceptedWebsockets.push(ws);
return ws;
}
Then, inside your WebView context you can an access array of instantiated websocket objects using window.interceptedWebsockets

Related

WKWebView iOS 16 Web Share API on file: protocol

I am attempting to use navigator.share in a file running in WKWebView on iOS 16. The file is loaded into the web view using a file: protocol path like so:
uiView.loadFileURL(Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "web")!, allowingReadAccessTo: Bundle.main.bundleURL)
I have this setting on:
configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
So I am able to load file: protocol files into the web view, but I don't seem to be able to use the Web Share API. I get this error:
NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.
However, when I point the web view at this HTTPS URL, it works:
https://mdn.github.io/dom-examples/web-share
This leads me to believe the Web Share API is not working because the HTML file loaded over the file: protocol is not viewed as secure and thus the JavaScript runtime don't treat the code on it as if it was running in a secure context.
As a result, I believe the access to the navigator.share API is forbidden.
Is there a way to configure WKWebView to allow access to secure context JS/DOM/web APIs without using a custom scheme (and adding an extra layer atop just loading my file from the bundle)?
The problem was not with the file: protocol or secure context.
I looked through WebKit source code for navigator.share and found that NotAllowedError gets returned whenever there don't seem to be conditions met for transient activation.
https://github.com/WebKit/WebKit/blob/d9bcb08521ddb86af9a713213d15488712919143/Source/WebCore/page/Navigator.cpp#L172
void Navigator::share(Document& document, const ShareData& data, Ref<DeferredPromise>&& promise)
{
if (!document.isFullyActive()) {
promise->reject(InvalidStateError);
return;
}
if (!validateWebSharePolicy(document)) {
promise->reject(NotAllowedError, "Third-party iframes are not allowed to call share() unless explicitly allowed via Feature-Policy (web-share)"_s);
return;
}
if (m_hasPendingShare) {
promise->reject(InvalidStateError, "share() is already in progress"_s);
return;
}
auto* window = this->window();
if (!window || !window->consumeTransientActivation()) {
promise->reject(NotAllowedError);
return;
}
if (!canShare(document, data)) {
promise->reject(TypeError);
return;
}
Transient activation is the thing where some web APIs will only run in response to a user gesture; the user gesture is either a part of the call stack leading up to the API call invocation or a user gesture has happened recently.
More about transient activation here:
https://developer.mozilla.org/en-US/docs/Glossary/Transient_activation
In my case I did have a user gesture there but also a WebRTC peer connection ICE gathering flow which while seemingly quick I guess lasted too long and it reset the transient activation flag preventing me from using the Web Share API at the end of the ICE gathering phase.
The fix is to prepare whatever you need for the Web Share API data payload in advance and in response to the user gesture, such as a button click, call the web API straight away.

Redirect API call fetches from Service Worker

This is a really annoying issue. I am using a third party login in my application. When a user logins in through the third party, it redirects an api call to the server.
ex: /api/signin/github?code=test&state=test
For some strange reason this API call is getting fetched from the service worker instead on the server which handles the login logic.
ex:
Without seeing your service worker's fetch event handler, it's hard to say exactly what code is responsible for that.
In general, though, if there are URLs for which you want to tell the service worker never to respond to, you can just avoid calling event.respondWith(...) when they trigger a fetch. There are lots of ways to avoid doing that, but an early return is straightforward:
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname === '/api/signin/github') {
// By returning without calling event.respondWith(),
// the request will be handled by the normal browser
// network stack.
return;
}
// Your fetch event response generation logic goes here.
event.respondWith(...);
});

Bypass Service-Worker caching

I have a progressive web-app, which speaks to an API. The calls to this api get cached by a service worker, which works great.
But now, I want to add a reload-button, which ideally forces the service worker to try to bypass the cache and update it if successful, also it should not return the cached result if a connection could not be made.
I am a bit unsure how to solve this. I am using the sw-toolbox.
All requests go through the fetch callback which receives a request object. Thus, before returning a cached response you can look for an additional header parameter (you need to include it into your request to API) to skip the logic returning cached response.
Based on your description, you are using the application cache. It can be accessed from the app fronted independent of the sw-tool box.
function onReloadButtonClicked(event) {
//Check for browser cache support
if ('caches' in window) {
//Update cache if network query is successful
caches.open('your_cache_name')
.then(function(cache) {
cache.add('your_url');
}).catch(function(err) {
// Do something with the error
});
}
}

UIWebView slow loading using NSUrlProtocol Xamarin.Forms

I'm working on an iOS application using Xamarin.Forms. This application is using UIWebView controller that shows a web application that is hosting on my server. Each time that I make a request I have to send a custom header in order to identify that this request comes to the mobile application and not from a browser, to do this I'm using an NSUrlProtocol object that overrides the method Request that inserts the custom header on each request.This is my code:
public override NSUrlRequest Request {
get {
NSMutableDictionary headers = null;
if (null == base.Request.Headers) {
headers = new NSMutableDictionary ();
} else {
headers = new NSMutableDictionary (base.Request.Headers);
}
headers.Add(NSObject.FromObject(AppVariables.headerVariable), NSObject.FromObject (AppVariables.appVersion));
NSMutableUrlRequest newRequest = (NSMutableUrlRequest)base.Request.MutableCopy ();
newRequest.Headers = headers;
return newRequest;
}
}
The problem that I have right now is that I noticed since I started using the NSUrlProtocol the loading time of the pages is increasing a lot. Right now the loading is taking 10 seconds, before this implementation the page took 3 seconds approximately.
Can anyone please point out some helpful direction to overcome this??
I don't see any reasons for the delay in response time when you're using custom headers. Like Andreas mentioned in the comments, I believe it has to do with your server code. I would recommend profiling your server code.
Do you see similar results when you send the requests (with custom headers) from Fiddler or cURL?
Just like #AndreasPaulsson and #prashant had mentioned, server might be the culprit. I would recommend testing the API with tools like Postman and check the response speed. I would also recommend you to check ModernHttpClient by Paul C Betts. In iOS the library uses NSUrlSession.

Switch between different API hosts

I'm working on an app which primarily works with an API that will be installed in an internal system. The API is also accessible via the public internet. The client wants to allow users to enter both an internal and external (public internet) URL that the app will then connect to depending on availability of the internal and external URLs.
The app is basically done with the exception that it currently connects to the internal URL only for all it's API calls. I'm using AFNetworking with block-based completion/failure invocations for each API call.
Based on the logic that we have designed, the app will always check for the API's availability by querying for the server's current time. This is done by calling http://internal_url/api/time. If this API fails to return an appropriate respond, we'll switch to the external URL http://external_url/api/time and call the same API on that URL. If both fails, the app will inform the user accordingly and not perform any other queries to the API.
Without revealing too much, here's some code on how I the API calls are currently setup:
- (void)someAPIMethodCall:(NSDictionary *)parameters completionBlock:block failure:block {
// query /api/time and return the URL (internal/external) that is currently up
AFHTTPClient *client = [AFHTTPClient clientWithBaseURL:<url returned from above query>];
[client operationWithSuccess:block failure:block];
}
So my question would be: what is the best way to get the query /api/time method above to work? Obviously, this method needs to complete and return either the internal/external URL so that the subsequent actual API query could use. AFAIK, AFNetworking calls are block-based so it will return before the above /api/time returns. I've also thought of a separate class that uses NSURLConnection synchronously which will block the main-thread while it waits for the /api/time to return.
I'd like to tell you to simply use the same URL internally and externally (via DNS) but that's not what you want.
I think you're asking how to conditionally call the other url.
You want someAPIMethodCall to be asynchronous... so you don't want to block on the call to checking for the correct api to call.
Aside from caching the results so you don't have to do this every time, you simply want to call another block based method of your own that has a completion block which passes IN a parameter of the URL to call for your real query.
- (void)someAPIMethodCall:(NSDictionary *)parameters completionBlock:(void (^)(void))succesBlock failure((^)(void)):failureBlock {
[self callBlockWithMyApiUrl:^(NSString *apiUrl){
AFHTTPClient *client = [AFHTTPClient clientWithBaseURL:apiUrl];
[client operationWithSuccess:successBlock failure:failureBlock];
} onFailure:^{
failureBlock
}
}
- (NSString *)callBlockWithMyApiUrl:(NSString * (^)(void))success (void (^)(void))failure
{
// Your code to test for the working URI
// If you're doing it this way, I'd suggest caching the result.
// Subscribe to networking interface changes to dump the cache.
}

Resources