WKWebView AJAX calls losing cookies - ios

Our customer requested us to do a switch from WebView to WKWebView recently. Their app uses native login, which is done by a 2 POST calls to their backend, returning various authorization cookies that were later on used in every single HTTP/AJAX call throughout the whole app.
With the WebView, it all worked like a charm without a need to implement a single line of custom code. User logged in, cookies were stored to cookie storage by default, and WebView always took them from there and used them, since the HTTPCookieStorage was shared between NSURLSession and WebView.
It it a whole new story with WKWebView. Once we switched WebView to WKWebView, we saw that the authorization was not working. It was due to losing some cookies in the WKWebView. We store the cookies from the NSURLSession response now and append them to the WKWebView manually, by adding "Cookie" header to the HTTP requests.
We were able to get the authorization for HTTP calls work this way, but now we are seeing a new problem. Somehow, all the AJAX calls done in the WKWebView lose the authorization cookies.
Do you please know if there is any way to somehow have the authorization cookies appear in the AJAX calls too? Injecting javascript with
javascriptCookieString = #"document.cookie = 'testCookie=testValue';";
[self.webView evaluateJavaScript:javascriptCookieString completionHandler:nil];
did not work and it seems like there is no control whatsoever over Javascript calls, so I cannot alter the requests before they are being executed. Thank you.

I found that the following snippet did the trick for us. We had the same problem.
// add session cookie to ajax calls
WKUserContentController* userContentController =
WKUserContentController.new;
WKUserScript * cookieScript =
[[WKUserScript alloc]
initWithSource: [[User sharedInstance] getJavscriptCookieString]
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init];
webViewConfiguration.userContentController = userContentController;
webViewConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true;
_wk = [[WKWebView alloc] initWithFrame:self.view.frame configuration:webViewConfiguration];
The cookie string needs to be formated correct in order to be accepted.
-(NSString*) getJavscriptCookieString {
return [NSString stringWithFormat: #"document.cookie = '%#=%#'", [self getSessionName], [self getSessionValue]];
}
Hope this can be of some help.
See also:
Can I set the cookies to be used by a WKWebView?

Related

How to handle CloudFront Signed Cookie with .m3u8 files on iOS?

I have an App which is playing audio files stored on AWS.
The audio content is secured by AWS CloudFront signed cookies functionality. I had no problem in creating the signed cookies and set them in the original HTTP request using AVURLAsset, and everything works just fine for .mp3 content. Nevertheless, when accessing .m3u8 files, I get a 403 HTTP error. I noticed that the initial request is ok and the .m3u8 file is downloaded correctly, but the subsequent requests (for the audio fragments) do not work and receive a 403 as the cookie is not sent.
I already tried using the NSHTTPCookieStorage, but it did not work ;-(
NSURL *url = [NSURL URLWithString: #"http://..../stream.m3u8"]
// Get the Cookie Storage
NSHTTPCookieStorage *cookiesStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
// Create the array to store the cookies
NSMutableArray *cookies = [NSMutableArray array];
// Create the Cloud-Front-Policy Cookie
NSHTTPCookie *cloudFrontPolicyCookie = ...
[cookies addObject:cloudFrontPolicyCookie];
// Create the Cloud-Front-Signature Cookie
NSHTTPCookie *cloudFrontSignatureCookie = ...
[cookies addObject:cloudFrontSignatureCookie];
// Create the Cloud-Front-Key-Paid-Id Cookie
NSHTTPCookie *cloudFrontKeyPairIdCookie = ...
[cookies addObject:cloudFrontKeyPairIdCookie];
// Create the HTTP Header Dictionary
NSMutableDictionary * headers = [NSMutableDictionary dictionary];
// I omitted the cookie creation, but it is ok! I tested using curl on the command line
NSString *cookieAsHeader = ...
// Set the header
[headers setObject:cookieAsHeader forKey:#"Cookie"];
// Create the AVURLAsset so that I can use the headers and the cookies
// Notice that I tried using only the headers (which works)!
AVURLAsset * asset = [AVURLAsset URLAssetWithURL:url options:#{#"AVURLAssetHTTPHeaderFieldsKey": headers, AVURLAssetHTTPCookiesKey : [cookiesStorage cookies] }];
// For secured .mp3 files, it works just fine
// but for .m3u8, the content does not play as the first file part receives 403. Notice that the first request (for the .m3u8) works just fine.
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
You need to generate the cookies, unfortunately aws ios sdk doesn't have this function. My temporary solution for now is one the the following:
1) Ask web server to generate the cookies (using aws php sdk). In this way you can set timeout in a few minutes or anytime you desire, so you can keep the data safe.
2) Hardcoded the cookies inside the code. You can generate it locally using aws php sdk, and then set duration as long as possible. Maybe a year or more depend on your condition. However if someone can see it, your data will be exposed.
3) Create our own cookies generator function in swift/obj-c. We can use aws php sdk for reference.
It looks like would be a good way to handle it: https://github.com/ykying/Swift_Player_AWS_Cookie
You will likely need to generate the cookies on whatever server you have written for serving content and whenever the user opens the app or logs in, give them the cookies with a 24 hour expiration (or however long your current session TTL is).
I hope this helps someone else, we're going to try this out in our app as well.

Intercept responses from WebSocket connection in UIWebView

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

UIWebView not displaying my webpage

For some background info, the webpage I'm trying to display is a web app currently being hosted on AWS's EC2. The backend is Python w/ Flask and the frontend is just simple HTML/CSS. The URL has HTTP, as it isn't secured with HTTPS yet. When the url for this webpage is opened, the first thing the browser asks is for login credentials (the browser asks, not the website). This page does load in mobile Safari on my iPhone, and Safari does successfully ask for the credentials. If I enter them in correctly, it will correctly load the page.
So I've tried both Allow Arbitrary Loads under App Transport Security Settings as well as a customized Exception Domain with the following keys:
App Transport Security Settings Dictionary
Exception Domains Dictionary
my website URL Dictionary
NSIncludesSubdomains Boolean (YES)
NSExceptionAllowsInsecureHTTPLoads Boolean (YES)
NSThirdPartyExceptionAllowsInsecureHTTPLoads Boolean (YES)
NSExceptionMinimumTLSVersion String (TLSv1.0)
NSExceptionRequiresForwardSecrecy Boolean (YES)
However, whenever I launch the app on the simulator all I'm getting back is a white screen (can post screenshot if needed).
Here's my code in ViewController.swift:
import UIKit
class ViewController: UIViewController {
#IBOutlet var WebView: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
let url = NSURL(string: "My URL inserted here")
let request = NSURLRequest(URL: url!)
WebView.loadRequest(request)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
If I use Allow Arbitrary Loads, when I look in the output box, it does not say "App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file." When I configure the Exception Domain correctly (with Allow Arbitrary Loads removed) it won't give me the message either. Only sometimes when I change around the settings using Exception Domain (again, with Allow Arbitrary Loads removed) do I get this output.
I'm starting to think the issue goes beyond security, and any advice or steps I could take to try and fix this issue would be much appreciated, thanks!
The white screen is a bit odd, assuming that a 401 would result in a standard error page, but maybe the server set up a white page for this.
My guess is that setting username and password directly in the URL doesn't work, you shouldn't do that anyways, but instead rely on WKWebView's webView:didReceiveAuthenticationChallenge: delegate method.
Here's some sample code hopefully working/helping:
#import "ViewController.h"
#import WebKit;
#interface ViewController () <WKNavigationDelegate>
#property (nonatomic, strong) WKWebView *webView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:[WKWebViewConfiguration new]];
self.webView.navigationDelegate = self;
[self.view addSubview:self.webView];
[self.webView setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-0-[_webView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(_webView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-0-[_webView]-0-|"
options:NSLayoutFormatDirectionLeadingToTrailing
metrics:nil
views:NSDictionaryOfVariableBindings(_webView)]];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSURL *target = [NSURL URLWithString:#"http://yourhost.com/possiblePage.html"];
NSURLRequest *request = [NSURLRequest requestWithURL:target];
[self.webView loadRequest:request];
}
- (void)webView:(WKWebView *)webView
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
NSURLCredential *creds = [[NSURLCredential alloc] initWithUser:#"username"
password:#"password"
persistence:NSURLCredentialPersistenceForSession];
completionHandler(NSURLSessionAuthChallengeUseCredential, creds);
}
#end
This is basically the implementation file of a simple ViewController (like from the single view template of XCode). It also shows you how you can add a WKWebView. Definitely make sure to check out all the delegate methods and such so you know what the thing can do for you.
Obviously, password and username have to be set somehow, I guess you can use a simple alert popup to have the user enter this info (this would be similar to Safari in principle). For the first test you can just hardcode it. Also note I set a sample subpage there, just use the exact same URL you would usually use on a desktop browser. Oh, and since the server doesn't have SSL, you need to allow arbitrary loads.
Edit:
RPM gave a good related comment below (thanks) that I had not originally thought about. The method may (actually will very likely) be called multiple times. This ultimately also depends on the website you load. RPM's explanation for why a site may appear plain white is spot on.
In any way, the webView:didReceiveAuthenticationChallenge:completionHandler: method above is just a simple example assuming you know the PW and username. Generally it will be more complex and you shouldn't just open an input dialog every time it is called for the user to enter credentials. As a matter of fact, the provided challenge offers ways to set a specific call to this delegate method into relation to previous calls. For example, it has a proposedCredential property that may already have been set. (Whether that's the case for loading multiple resources I don't know on the top of my head, just try that out.) Also, check its previousFailureCount, etc. A lot of this may depend on the site you load and what it needs to get.

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.

Custom Login Instagram API iOS

I'm developing an application for iOS with the authentication, using Instagram API.
My code for authentication works good:
NSString *fullURL = [NSString stringWithFormat:#"https://instagram.com/oauth/authorize/?client_id=%#&redirect_uri=%#&response_type=code",KCLIENTID,kREDIRECTURI];
NSURL *url = [NSURL URLWithString:fullURL];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
instagramConnectWebView = [[UIWebView alloc] initWithFrame:self.view.bounds];
instagramConnectWebView.delegate = self;
[instagramConnectWebView loadRequest:request];
When the request is sent, my webView shows the standard Instagram login interface, where user has to insert username and password to login. If login succeed, app is redirected to my redirection-url, where i get the code and the token of this user.
My question is really simple, is there a way to customize standard interface of Instagram login? I'd like to insert in my app two simple UITextField (One for username and one for the password) and when user clicks on my UIButton login, application should send (POST or GET?) username and password to Instagram, and return response (Login success or not).
I hope i explained myself.
Thanks.
You can make this by using javascript. by loading a web view in background :
You need to access the text field of that web view and pass value of your custom UItextfields and after that you can call webView's "Submit" button's Onclick event.
NSString *javaScript = #"document.getElementById('id_username').value = 'tfusername'";
NSString *javaScript2 = #"document.getElementById('id_password').value = 'tfpassword'";
to evaluate each script you can call: [webView stringByEvaluatingJavaScriptFromString:javaScript];
You can also call script to press login button. This same thing work for me. May be this will help you.
I have created a wrapper for Instagram API for iOS. Take a look at this link . This will also give an idea of what you need to do.

Resources