Intercept request with WKWebView - ios

Now i'm using UIWebView and with canInitWithRequest: of NSURLProtocol i can intercept all requests and do with it what I want.
In the new WKWebView this method there isn't, and i not found something similar.
Has someone resolved this problem?

I see that after 5 years this question still generates curiosity, so I describe how I solved it and about some main problems I faced up.
As many who answered here, I have implemented WKURLSchemeHandler and used new schemes.
First of all the URL that wkwebview launches must not be HTTP (or HTTPS) but one of yours new schemes.
Example
mynewscheme://your-server-application.com
In you WKWebViewConfiguration conf, I set the handler:
[conf setURLSchemeHandler:[CustomSchemeHandler new] forURLScheme:#"mynewscheme"];
[conf setURLSchemeHandler:[CustomSchemeHandler new] forURLScheme:#"mynewschemesecure"];
In CustomSchemeHandler I have implemented webView:startURLSchemeTask: and webView:stopURLSchemeTask:.
In my case I check if the request is for a file that I just saved locally, otherwise I change actual protocol ("mynewscheme or "mynewschemesecure") with http (or https) and I make request by myself.
At this point I solved the "interception problem".
In this new way we have the webview "location" (location.href via javascript) with my new scheme and with it new problems started.
First problem is that my applications work mainly with javascript,
and document.cookie has stopped working. I'm using Cordova
framework, so I've develeped a plugin to set and get cookie to
replace document.cookie (I had to do this, because, obviously, I
have also http header set-cookie).
Second problem is that I've got a lot of "cross-origin" problems, then
I changed all my urls in relative url (or with new schemes)
Third problem is that browser automatically handle server port 80
and 443, omitting them, but has now stopped (maybe because of "not
http location"). In my server code I had to handle this.
Writing down these few rows I admit that it seems to was an easy problem to solve, but I ensure that find out a workaround, how to solve it and integrate with the infinite amount of code has been hard. Every step towards the solution corresponded to a new problem.

You can intercept requests on WKWebView since iOS 8.0 by implementing the decidePolicyFor: navigationAction: method for the WKNavigationDelegate
func webView(_ webView: WKWebView, decidePolicyFor
navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Swift.Void) {
//link to intercept www.example.com
// navigation types: linkActivated, formSubmitted,
// backForward, reload, formResubmitted, other
if navigationAction.navigationType == .linkActivated {
if navigationAction.request.url!.absoluteString == "http://www.example.com" {
//do stuff
//this tells the webview to cancel the request
decisionHandler(.cancel)
return
}
}
//this tells the webview to allow the request
decisionHandler(.allow)
}

there are many ways to implement intercepter request.
setup a local proxy, use WKNavigationDelegate
-[ViewController webView:decidePolicyForNavigationAction:decisionHandler:]
load new request and forward to local server, and at the local server side you can do something(eg. cache).
private api. use WKBrowsingContextController and custom URLProtocol
Class cls = NSClassFromString(#"WKBrowsingContextController");
SEL sel = NSSelectorFromString(#"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
// 把 http 和 https 请求交给 NSURLProtocol 处理
[(id)cls performSelector:sel withObject:#"http"];
[(id)cls performSelector:sel withObject:#"https"];
}
use KVO to get system http/https handler.(ios 11, *)
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
URLSchemeHandler *schemeHandler = [[URLSchemeHandler alloc] init];
[configuration setURLSchemeHandler:schemeHandler forURLScheme:#"test"];
NSMutableDictionary *handlers = [configuration valueForKey:#"_urlSchemeHandlers"];
handlers[#"http"] = schemeHandler;
handlers[#"https"] = schemeHandler;
all the three ways you probably need handle CORS & post bodies to be stripped,you overwrite change browser`s option request(
Preflighted_requests),you can custom http header to replace http body for post bodies to be stripped.

in iOS 11 WKWebView has come up with Custom Scheme Handler called WKURLSchemeHandler, which you can use to intercept the custom events.
for more info check out this project.
https://github.com/BKRApps/KRWebView

I know I am late but I am able to solve this problem. I can intercept each and every request even your http/https call using below trick. I can also trace the call made from html to server calls. I can also use this to render html with offline content.
Download the html of the website that we want to render in offline or online to intercept the request.
Either place the html in document directory of the user or place it inside the archive. But we should know the path of the html file.
Place all your js, cs, woff, font of our website at the same level as our base html. We need to given permission while loading the web view.
Then we have to register our own custom handler scheme with WKWebView. When wkwebview see the pattern "myhandler-webview" then it will give you control and you will get the callback to 'func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask)' delegate implementation. You can play around with url in this delegate like mentioned in point 6.
let configuration = WKWebViewConfiguration();
configuration.setURLSchemeHandler(self, forURLScheme: "myhandler-webview");
webView = WKWebView(frame: view.bounds, configuration: configuration);
Convert file scheme to the custom scheme (myhandler-webview) then load it with WKWebView
let htmlPath = Bundle.main.path(forResource: "index", ofType: "html")
var htmlURL = URL(fileURLWithPath: htmlPath!, isDirectory: false)
htmlURL = self.changeURLScheme(newScheme: "myhandler-webview", forURL: htmlURL)
self.webView.load(URLRequest(url: htmlURL))
Implement below methods of WKURLSchemeHandler protocol and handle didReceiveResponse, didReceiveData, didFinish delegate methods of WKURLSchemeTask.
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
print("Function: \(#function), line: \(#line)")
print("==> \(urlSchemeTask.request.url?.absoluteString ?? "")\n")
// You can find the url pattern by using urlSchemeTask.request.url. and create NSData from your local resource and send the data using 3 delegate method like done below.
// You can also call server api from this native code and return the data to the task.
// You can also cache the data coming from server and use it during offline access of this html.
// When you are returning html the the mime type should be 'text/html'. When you are trying to return Json data then we should change the mime type to 'application/json'.
// For returning json data you need to return NSHTTPURLResponse which has base classs of NSURLResponse with status code 200.
// Handle WKURLSchemeTask delegate methods
let url = changeURLScheme(newScheme: "file", forURL: urlSchemeTask.request.url!)
do {
let data = try Data(contentsOf: url)
urlSchemeTask.didReceive(URLResponse(url: urlSchemeTask.request.url!, mimeType: "text/html", expectedContentLength: data.count, textEncodingName: nil))
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
} catch {
print("Unexpected error when get data from URL: \(url)")
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
print("Function: \(#function), line: \(#line)")
print("==> \(urlSchemeTask.request.url?.absoluteString ?? "")\n")
}
Let me know if this explanation is not enough.
Objective c example mentioned below
intercepting request with wkwebview

One can use WKURLSchemeHandler to intercept each and every request to be loaded in WKWebView,
Only disadvantage is that you cannot register http or https scheme for interception,
Solution over that is,
Replace your http/https scheme of url with custom scheme url like xyz://
for e.g. https://google.com can be loaded like xyz://google.com
Now you will get a callback in WKURLSchemeHandler there you again replace it back to https and load data programmatically and call urlSchemeTask.didReceive(response)
This way each and every https request will come to your handler.

I am blindly taking guesses since I only have my windows computer with me. By reading the Apple Developer documentation here is information I gathered that might lead to some ideas on how to solve the question.
Based on WKWebView,
Set the delegate property to an object conforming to the WKUIDelegate protocol to track the loading of web content.
Also, I see we can set our navigationDelegate with the,
weak var navigationDelegate: WKNavigationDelegate? { get set }
The methods of the WKNavigationDelegate protocol help you implement custom behaviors that are triggered during a web view's process of accepting, loading, and completing a navigation request.
Then after we create and set our custom WKNavigationDelegate, we would override some methods to intercept something we might be looking for. I found the Responding to Server Actions section of some interest since they receive a WKNavigation as parameter. Moreover, you might want to skim through WKNavigationAction and WKNavigationResponse see if there is perhaps something which might help us achieve our goal.
BTW, I am just giving some ideas on what to try so that we can solve this question, ideas which might be 100% wrong cause I have not tried them myself.

Related

How to get a final URL after t.co redirects in WKWebView

I know that I can generally get the current URL of WKWebView by using the URL property. However I have discovered that when there is a redirect, it will not give me the proper URL.
For example, if I go to http://twitter.com and then click on a link to some other company (ex: http://mycompany.com), then I see a t.co/XXX URL which eventually redirects me to mycompany.com.
However, when I look at WKWebView's URL property, I am seeing "t.co" instead of "mycompany.com".
Strangely, I am never seeing didReceiveServerRedirectForProvisionalNavigation:... called, and when I check URL at didStartProvisionalNavigation:... and decidePolicyForNavigationAction:... I just see the "t.co" URL instead of the "mycompany.com" one.
Also, I will need to know the domain in order to make some adjustments to the body, so I am not sure if I can use JS here.
Please let me know if you have any ideas.
UPDATE: I realized this only happens when I use a custom URL scheme set via setURLSchemeHandler:, which I had omitted from the question originally since I didn't think it was related.
After some playing around, I was able to get things working with the following change in willPerformHTTPRedirection:...:
NSMutableURLRequest *newRequest =
[NSMutableURLRequest requestWithURL:request.URL];
dispatch_async(dispatch_get_main_queue(), ^{
[_webView loadRequest:newRequest];
});
completionHandler(nil);
UPDATE: This has a serious drawback because URLs that are not for the main frame will come into willPerformHTTPRedirection:..., and if the request is re-loaded for all of these the page gets messed up.
I need a way to determine if the URL is from the main frame and only do the reload for those.
Alright, so i made it. https://imgur.com/a/eBSaNn7
The solution is to cancel the navigation and load the request with loadRequest: again.
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if navigationAction.targetFrame?.isMainFrame != true {
webView.load(navigationAction.request)
}
return nil
}

Closes SFSafariViewController on a certain url

I am trying to close SFSafariViewController when I reach a certain page.
But I am unable to do it until the "Done" button is pressed explicitly
by the user.
What I want is to get the URL as soon as it reached a certain page and then dismiss the view controller.
Then I can pick the rest using this
func safariViewControllerDidFinish(_ controller: SFSafariViewController){
// do work here
}
Note: This question has been asked before but I was unable to find any
satisfactory answers
If you register a URI scheme for your app, you could have have a link on a page which takes you back to your app. At that point you'll be in
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
of your AppDelegate and you can do whatever you wish.
“Because since Safari View Controller has access to a user’s
credentials, synced across all of their devices with iCloud Keychain,
logging in is going to be a breeze.[…] It takes two steps. The first
is where you would’ve used your own in-app browser, just present an
instance of SFSafariViewController.
And once the user is finished logging in and the third-party web
service redirects back to your app with the custom URL scheme that you
fed it, you can accept that in your AppDelegate‘s handleOpenURL
method.
From there you can inspect the response and dismiss the instance of
SFSafariViewController because you know that the authentication is
done.
That’s it. Two steps.”
Source: http://asciiwwdc.com/2015/sessions/504#t=1558.026
Please note the handleOpenURL method is deprecated in favor of application:openURL:options:.
Maybe It's late But I think It will help anyone search for the same question
Listening to the SFSafariViewControllerDelegate delegate you can use this method inside
func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) {}
inside it you can make if condition on your certain page you need to take action for ... as example if I want to close it when reaching certain page I'will fo this
func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo URL: URL) {
if URL.absoluteString == "WHATEVER YOUR LINK IS"{
controller.dismiss(animated: true, completion: nil)
}
}
You cannot do that with a SFSafariViewController. Use a WKWebView instead and implement the WKNavigationDelegate method like optional func webView(_ webView: WKWebView,
didFinish navigation: WKNavigation!) and then read the current URL. Define your action based on that.
You can use this to close the view controller that houses your web view. You will unfortunately have to build your own navigation buttons (the ones the SFSafariViewController has), if you want to allow the user access to those. You don't have to provide them.

WKWebView, get all cookies

I want obtain all cookies from WKWebView. Why? I have been started a project that use web-based auth. As result, I should intercept cookies to be sure that user is logged in and for some other purposes. Another case - imagine if user logged in, and than he "kill" the app - due to some delay in storing this cookie session will be lost :(.
The problem seems to be that the cookies are cached and not saved out
to a file immediately.
(#Kemenaran from here - p.5 below)
The point where I try to catch them -
webView:decidePolicyForNavigationResponse:decisionHandler:,
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
if let httpResponse = navigationResponse.response as? NSHTTPURLResponse {
if let headers = httpResponse.allHeaderFields as? [String: String], url = httpResponse.URL {
let cookies = NSHTTPCookie.cookiesWithResponseHeaderFields(headers, forURL: url {
for cookie in cookies {
NSHTTPCookieStorage.shared.set(cookie)
}
}
}
}
but not all request are navigation, so one cookie (in my case) is skipped, see details below
Few words about other option I tried...
Yes, i Know that starting from iOS 11, we can use WKHTTPCookieStore as mention here. But my project should support iOS 9+
I for 100% sure, that after 5-10 sec from login, required cookie will be saved to NSHttpCookieStorage (at least all my tests during few days confirm that)
I try to use provided observer NSHTTPCookieManagerCookiesChangedNotification, but it provide me callback only for cookies that comes within webView:decidePolicyForNavigationResponse:decisionHandler
I also try to get cookies using some JS like mentioned here and also test all suggestion from here - really great article by the way. Result - negative
I also found this radar bug, and this SO question, and Sample project, but I want to prevent even this case. (described in this post applicable not only for remove but and for save) Also this situation true and when user kill the app, so case when user login, kill app and relaunch, may be present. And preventing this (simply by checking NSHttpCookieStorage for required cookies are also not good idea, because exactly after login required cookie can be stored with some delay, so this approach requires some bool-powered solution, that looks like weird..
I also read few more SO post for some related problem, and the most usefull are
This one
Another one
One more
But still without good solution...
So, is any way exist to obtain or at least force to immediately store cookies?
I ended with simple "force-like" saving Cookie from webpage.
To get all cookie i use
stringByEvaluatingJavaScriptFromString
with JS string like document.cookie();. As result i able to receive all cookies as a string with ; separator. All i need to do - parse string, create cookie and set it to NSHttpSharedStorage

Swift - WebView HTTP Auth - Cleanest solution

I have been trying to set up a web view wrapper app that will load the content of a website (still to be launched). Currently, the website is in development mode, and the only endpoints for the website are protected behind a http authentication.
I have been looking at this solution: Swift webview xcode post data
However, I do not want to make a POST request each time, but I'd rather want to authenticate against the website once and keep the connection.
What I'm looking is for a clean and stable solution, one that would allow me to be able to have control of edge cases such as bad credentials provided.
I am not comfortable with using the NSURLConnection because that solution is deprecated in iOS9. I need a solution with NSURLSession.
Let me know if I'm missing something within the above linked solution. I am sure someone had this issue as well. Additionally, the website has SSL protection.
Kind regards
I'm not entirely sure this fulfils your demands in the best way, but if you can use a WKWebView, maybe you can simply rely on the authentication challenge delegate method? See my answer here as well, the relevant code snippet would be:
func webView(webView: WKWebView, didReceiveAuthenticationChallenge
challenge: NSURLAuthenticationChallenge,
completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
let creds = NSURLCredential(user:"username", password:"password", persistence: NSURLCredentialPersistence.ForSession)
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, creds)
}
I haven't tried it yet myself, but the documentation says the credentials should be used for the session, so additional requests resulting from links should work. Even if not, that just results for the method to be called again and you can provide the credentials once more.
This is just a rump, you'd have to get name and password from an alert or the like (also you can store the credentials more elegantly to make subsequent calls to the delegate method more elegant).
You also wrote you're using SSL, so I take it you're familiar with the App Transport Security flags (since the question title just has "HTTP" in it and not "HTTPS", which you probably want for it to work smoothly, otherwise see the question I linked).
Okay here is the Swift 3 code.
extension MyController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let u = self.webuser, let p = self.webp else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let creds = URLCredential.init(user: u, password: p, persistence: .forSession)
completionHandler(.useCredential, creds)
}
}
You shouldn't need to use POST or a connection or session. You should just create a mutable URL request and set an auth header. Then ask the web view to load the request.
Auth header details from Wikipedia:
The username and password are combined with a single colon.
The resulting string is encoded using the RFC2045-MIME variant of Base64, except not limited to 76 char/line.
The authorization method and a space i.e. "Basic " is then put before the encoded string.
For example, if the user agent uses Aladdin as the username and OpenSesame as the password then the field is formed as follows:
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l

How can I retrieve a file using WKWebView?

There is a file (CSV) that I want to download. It is behind a login screen on a website. I wanted to show a WKWebView to allow the user to log in and then have the app download the file after they had logged in.
I've tried downloading the file outside of WKWebView after the user has logged in to the website, but the session data seems to be sandboxed because it downloads an html document with the login form instead of the desired file.
I've also tried adding a WKUserScript to the WKUserContentController object, but the script doesn't get run when a non-HTML file is loaded.
Is there a way for me to access this file while allowing users to log in via the WKWebView?
Right now, WKWebView instances will ignore any of the default networking storages (NSURLCache, NSHTTPCookieStorage, NSCredentialStorage) and also the standard networking classes you can use to customize the network requests (NSURLProtocol, etc.).
So the cookies of the WKWebView instance are not stored in the standard Cookie storage of your App, and so NSURLSession/NSURLConnection which only uses the standard Cookie storage has no access to the cookies of WKWebView (and exactly this is probably the problem you have: the „login status“ is most likely stored in a cookie, but NSURLSession/NSURLConnection won’t see the cookie).
The same is the case for the cache, for the credentials etc. WKWebView has its own private storages and therefore does not play well with the standard Cocoa networking classes.
You also can’t customize the requests (add your own custom HTTP headers, modify existing headers, etc), use your own custom URL schemes etc, because also NSURLProtocol is not supported by WKWebView.
So right now WKWebView is pretty useless for many Apps, because it does not participate with the standard networking APIs of Cocoa.
I still hope that Apple will change this until iOS 8 gets released, because otherwise WKWebView will be useless for many Apps, and we are probably stick with UIWebView a little bit longer.
So send bug reports to Apple, so Apple gets to know that these issues are serious and needs to be fixed.
Have you checked the response cookies coming back from the request. You could use a delegate method like this.
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
for (NSHTTPCookie *cookie in cookies) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
I've accomplished something similar to what you're trying to do:
use the web view to login as you're already doing
set a navigationDelegate on your webview
implement -webView:decidePolicyForNavigationResponse:decisionHandler in your delegate
when that delegate method is called (after the user logs in), you can inspect navigationResponse.response (cast it to NSHTTPURLResponse*) and look for a Set-Cookie header that contains the session info you'll need for authenticated sessions.
you can then download the CSV by manually specifying the Cookie header in your request with the cookies specified in the response.
Note that the delegate methods are only called for "main frame" requests. Which means that AJAX requests or inner frames will not trigger it. The whole page must refresh for this to work.
If you need to trigger behavior for AJAX requests, iframes etc you'll need to inject some javascript.
You can obtain the cookie via Javascript:
You could then use the obtained cookie to download the file manually:
webView.evaluateJavaScript("(function() { return document.cookie })()", completionHandler: { (response, error) -> Void in
let cookie = response as! String
let request = NSMutableURLRequest(URL: docURL)
request.setValue(cookie, forHTTPHeaderField: "Cookie")
NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { (data, response, error) in
// Your CSV file will be in the response object
}).resume()
})
I do not seem to have the session cookie when I try a request from userContentController didReceiveScriptMessage.
However, when called from decidePolicyForNavigationAction, the following code detects that I'm logged in.
let urlPath: String = "<api endpoint at url at which you are logged in>"
let url = NSURL(string: urlPath)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url!, completionHandler: {data, response, error -> Void in
let string1 = NSString(data: data, encoding: NSUTF8StringEncoding)
println(string1)
println(data)
if(error != nil) {
println("Error sending token to server")
// Print any error to the console
println(error.localizedDescription)
}
var err: NSError?
})
task.resume()

Resources