This question already has answers here:
UIWebView to view self signed websites (No private api, not NSURLConnection) - is it possible?
(9 answers)
Closed 6 years ago.
We have an iOS app that uses a UIWebView to display content. We load it up with data with code that looks like this:
NSURL *url = [NSURL URLWithString:myURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[_webView setDelegate:self];
[_webView loadRequest:request];
This used to work fine with HTTP requests, but now we are using HTTPS against a server with a self-signed SSL certificate. When the above is run, the webView:didFailLoadWithError: delegate method gets called, with this error:
The certificate for this server is invalid. You might be connecting to a server that is pretending to be "blah.blah.blah.com" which could put your confidential information at risk."
I would like to simply ignore the invalid certificate and go on with the request, as one can do in Mobile Safari.
I have seen how to work around this issue when using NSURLConnection (see HTTPS request on old iphone 3g, for example), but what can one do with a UIWebView?
I imagine that I could rework the code so that it uses NSURLConnection to make the requests and then puts the results into the web view by calling its loadHTMLString:baseURL: method, but that's going to get complicated when the pages have images, CSS, JavaScript, and so on. Is there an easier way?
Please note: This API is currently unsupported, and should really only be used in a safe testing environment. For further details, take a look at this CocoaNetics article.
[NSURLRequest setAllowsAnyHTTPSCertificate:YES forHost:[url host]]; will allow you to ignore certificate errors. You will also need to add the following to the beginning of your file to grant you access to these private APIs:
#interface NSURLRequest (DummyInterface)
+ (BOOL)allowsAnyHTTPSCertificateForHost:(NSString*)host;
+ (void)setAllowsAnyHTTPSCertificate:(BOOL)allow forHost:(NSString*)host;
#end
Just so everyone knows... the above use of hidden interfaces WILL NOT BE ACCEPTED BY APPLE. They look for use of private APIs and it is NOT an acceptable solution. So, please do not go posting the solution described above around as THE way to fix it because, although it works, it will buy you a rejection in the AppStore. That makes it useless.
What follows is the ACCEPTABLE method of ignoring invalid server certificates. You need to use NSURLConnection and load the data for the webpage manually like so:
.
.
.
//Create a URL object.
url = [NSURL URLWithString:urlAddress];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:requestObj delegate:self];
[connection start];
}
And then, in your delegate....
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
}
else
{
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[resultData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *htmlString = [[NSString alloc] initWithBytes:[resultData bytes] length:[resultData length] encoding:NSUTF8StringEncoding];
[webView loadHTMLString:htmlString baseURL:url];
}
#end
Where resultData is an NSMutableData you instantiated earlier and where url and urlAddress are both things you've instantiated and filled in elsewhere.
Unfortunately, I currently don't know a way to get the actual UIWebView to load a page directly without having a valid certificate.
Yours, GC
It turns out that once the site is authenticated by a cancelled NSURLConnection, the UIWebView can make requests to the site. There is a complete explanation here.
As far as I know, that isn't possible with just UIWebView. As I understand it, you need to use NSURLConnection to handle all the HTTP/HTTPS mojo and then feed its results to the UIWebView via -loadHtmlString:baseURL: or -loadData:MIMEType:textEncodingName:baseURL:.
Related
I am new to iOS development. I have a project where I have to use WKWebView instead of UIWebView. Its a simple webpage with javascript to Objective-C and other way round integration. It is working fine except when I try to open a server with self-signed certificate. On Safari it shows a dialog box where we can choose to continue. However on my Application I cannot bypass this.
For some reason I have to run it with self-signed certificate.
The Delegate method didReceiveAuthenticationChallenge is never called. Other delegate methods are working fine. I know didReceiveAuthenticationChallenge is depreciated in iOS8 but can someone please tell me the workaround for this. As I am a newbie so a complete working delegate method and/or any other changes in the code will be highly appreciated.
NSURL *url = [NSURL URLWithString:#"https://mywebsite.com/Default.aspx"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
[[self webView] loadRequest:request];
}
// And the delegate method that is not getting called is
- (void)webView:(WKWebView *)webView
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
CFDataRef exceptions = SecTrustCopyExceptions(serverTrust);
SecTrustSetExceptions(serverTrust, exceptions);
CFRelease(exceptions);
completionHandler(NSURLSessionAuthChallengeUseCredential,
[NSURLCredential credentialForTrust:serverTrust]);
}
This has been confirmed as a bug by Apple. It has been fixed in iOS9.
In the code snippet you posted, i didn't see you were setting the class as the navigationDelegate of the webview..
Your view controller should implement the protocol WKNavigationDelegate and you need to add the below snippet for the delegate to be invoked..
[self webView].navigationDelegate = self;
The standard examples I'm seeing in iOS to do a simple GET HTTP connection is to:
NSString *finalURLstring = #"http://www.somesite.com?value=2";
NSURL *url = [NSURL URLWithString:finalURLstring];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSURLConnection *urlConnection = [NSURLConnection connectionWithRequest:request delegate:self];
Then implement these functions:
-(void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
-(void) connection:(NSURLConnection*)connection didReceiveData:(NSData*)data
-(void) connectionDidFinishLoading:(NSURLConnection*)connection
And if you have multiple connections you just create multiple NSURLConnections and compare them in connectionDidFinishLoading:
if (connection == urlConnection1)
// do something
else if (connection == urlConnection2)
// do something else
The issue with this is that you are created a new connection to the server each time right? Is it possible to create your NSURLConnection and reuse it? i.e. keep the connection open so you can make multiple calls instead of creating new connections on every call to the server?
Thanks for your help,
-David
NSURLConnection does that already. It reuses the existing connections.. Check out this accepted answer in
this post. You'll get your answer for your question..
This question already has answers here:
How to use iOS Reachability
(3 answers)
Closed 9 years ago.
I'm writing an app for a college that will likely be used on campus. The college's wifi requires credentials to access the internet through a web page (such as AT&T hotspots). I would like my app to detect whether it's 'connected' to the internet or not. In the past, I've seen other applications redirect to Safari so the user can authenticate and then go back to the application. Does anyone know how to detect this sort of thing without simply trying to grab NSData from a connection (such as google.com) and then assuming if no data is grabbed this is the issue?
iOS automatically brings up a web view when you are trying to connect to a network that has a captive portal. To make sure you are connected and authenticated in your app, you should set UIRequiresPersistentWiFi in your Info.plist.
EDIT: My answer above is only for apps that require an internet connection. If you're just checking whether you are connected and authenticated, I believe you just have to use Reachability and check that you are ReachableViaWiFi. (I believe SystemConfiguration will not say you are reachable via Wi-Fi if you are not authenticated.)
If you are looking to handle the captive network authentication in your app instead of in the iOS default web view, you can use the CaptiveNetwork API.
Since you don't want to use a NSData way (I don't like grabbing NSData either as well as using reachability) Here is what I came up with that is more lightweight because it just checks the HEAD response :) :
- (BOOL)connectedToInternet
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:
[NSURL URLWithString:#"http://www.google.com/"]];
[request setHTTPMethod:#"HEAD"];
NSHTTPURLResponse *response;
[NSURLConnection sendSynchronousRequest:request
returningResponse:&response error: NULL];
return ([response statusCode] == 200) ? YES : NO;
}
- (void)yourMethod
{
if([self connectedToInternet] == NO)
{
// Not connected to the internet
}
else
{
// Connected to the internet
}
}
If you don't want to use Reachability you could initiate a NSURLConnection to a random website while on the campus wifi and check for an authentication challenge.
Set up the NSURLConnection:
// Create the request.
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.apple.com/"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
// create the connection with the request
// and start loading the data
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
// Create the NSMutableData to hold the received data.
// receivedData is an instance variable declared elsewhere.
receivedData = [[NSMutableData data] retain];
} else {
// Inform the user that the connection failed.
}
Implement the auth challange delegate method:
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
NSLog(#"I'm being challenged.");
}
Then do what you want after the challenge.
Without going and looking at Reachability, if you try to use it to reach a host and it is presented with an auth challenge, it may return that there is no connection because it couldn't reach the specified host. Again, not 100% sure if this statement is accurate.
If a user attempts to load a https web page in Mobile Safari and the server's certificate validation check fails (its expired, revoked, self-signed etc.) then the user is presented is presented with a warning message and asked if they want to continue or not.
Similarly NSURLConnection offers the ability for the implementator to decide firstly how to check the certificate and then decide how to proceed if it fails, so in this situation too it would be possible to display a warning to the user and offer them the opportunity to continue loading the page or not.
However it seems when loading a https page in UIWebView that fails a certificate check the behaviour is just to fail to load the page - didFailLoadWithError: gets called with kCFURLErrorServerCertificateUntrusted however nothing gets displayed to the user.
This is inconsistent - surely the UIWebView behaviour should behave in a similar way to Safari to be consistent within iPhone itself?
Its also a daft that NSURLConnection allows total flexibility with this yet NSURLRequest:setAllowsAnyHTTPSCertificate is private.
Is there anyway to implement behaviour which is consistent with Safari, can this default behavior be customized in a similar way to NSURLConnection allows?
Cheers
P.S.
Please refrain from getting into patronizing side discussions about why would anybody want to do this, thank you very much.
I found out how to do this:
1) When the page is loaded it will fail, thus add something like the following to didFailLoadWithError:
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
if ([error.domain isEqualToString: NSURLErrorDomain])
{
if (error.code == kCFURLErrorServerCertificateHasBadDate ||
error.code == kCFURLErrorServerCertificateUntrusted ||
error.code == kCFURLErrorServerCertificateHasUnknownRoot ||
error.code == kCFURLErrorServerCertificateNotYetValid)
{
display dialog to user telling them what happened and if they want to proceed
2) If the user wants to load the page then you need to connect using an NSURLConnection:
NSURLRequest *requestObj = [NSURLRequest requestWithURL:self.currentURL cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10.0];
self.loadingUnvalidatedHTTPSPage = YES;
[self.webView loadRequest:requestObj];
3) Then make this change to shouldStartLoadWithRequest
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if (self.loadingUnvalidatedHTTPSPage)
{
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[self.connection start];
return NO;
}
4) Implement the NSURLConnectionDelegate as:
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
SecTrustRef trust = challenge.protectionSpace.serverTrust;
NSURLCredential *cred;
cred = [NSURLCredential credentialForTrust:trust];
[challenge.sender useCredential:cred forAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
NSURLRequest *requestObj = [NSURLRequest requestWithURL:self.currentURL cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:10.0];
self.loadingUnvalidatedHTTPSPage = NO;
[self.webView loadRequest: requestObj];
[self.connection cancel];
}
It all seems to work fine.
From the horse's mouth:
"UIWebView does not provide any way for an app to customize its HTTPS server trust evaluations. It is possible to work around this limitation using public APIs, but it is not easy. If you need to do this, please contact Developer Technical Support (dts#apple.com)
Source: http://developer.apple.com/library/ios/#technotes/tn2232/_index.html
I would like to support HTTP Basic Authentication in my UIWebView.
At the moment, I am canceling requests in
webView:shouldStartLoadWithRequest:navigationType: then handle them in my own NSURLConnectionDelegate to check for and provide credentials if needed. I then use loadData:MIMEType:textEncodingName:baseURL: to present HTML in the web view. That works fine for any URLs that are passed to the delegate.
My problem is that the delegate is never called for embedded elements, like images, JavaScript or CSS files. So if I have an HTML page which references an image which is protected with basic authentication, that image cannot be loaded properly. Additionally, webView:didFinishLoad: is never called, because the web view could not fully load the page.
I have checked that case with Terra, a third-party browser available on the App Store, and it can fully cope with that situation. I think it would be possible to solve this by providing my own NSURLProtocol, but that seems too complicated. What am I missing?
Try to use sharedCredentialStorage for all domains you need to authenticate.
Here is working sample for UIWebView it was tested against Windows IIS having only BasicAuthentication enabled
This is how to add your site credentials:
NSString* login = #"MYDOMAIN\\myname";
NSURLCredential *credential = [NSURLCredential credentialWithUser:login
password:#"mypassword"
persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc]
initWithHost:#"myhost"
port:80
protocol:#"http"
realm:#"myhost" // check your web site settigns or log messages of didReceiveAuthenticationChallenge
authenticationMethod:NSURLAuthenticationMethodDefault];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential
forProtectionSpace:protectionSpace];
[protectionSpace release];
Edit: same code in Swift 4
let login = "MYDOMAIN\\myname"
let credential = URLCredential(user:login, password:"mypassword", persistence:.forSession)
let protectionSpace = URLProtectionSpace(host:"myhost", port:80, protocol:"http", realm:"myhost", authenticationMethod:NSURLAuthenticationMethodDefault)
URLCredentialStorage.shared.setDefaultCredential(credential, for:protectionSpace)
Your webView is supposed to work now, if it does not work use next code to debug, especially check log messages of didReceiveAuthenticationChallenge.
#import "TheSplitAppDelegate.h"
#import "RootViewController.h"
#implementation TheSplitAppDelegate
#synthesize window = _window;
#synthesize splitViewController = _splitViewController;
#synthesize rootViewController = _rootViewController;
#synthesize detailViewController = _detailViewController;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// Add the split view controller's view to the window and display.
self.window.rootViewController = self.splitViewController;
[self.window makeKeyAndVisible];
NSLog(#"CONNECTION: Add credentials");
NSString* login = #"MYDOMAIN\\myname";
NSURLCredential *credential = [NSURLCredential credentialWithUser:login
password:#"mypassword"
persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc]
initWithHost:#"myhost"
port:80
protocol:#"http"
realm:#"myhost" // check your web site settigns or log messages of didReceiveAuthenticationChallenge
authenticationMethod:NSURLAuthenticationMethodDefault];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:protectionSpace];
[protectionSpace release];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://myhost/index.html"]
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:12
];
NSLog(#"CONNECTION: Run request");
[[NSURLConnection alloc] initWithRequest:request delegate:self];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
}
- (void)applicationWillTerminate:(UIApplication *)application
{
}
- (void)dealloc
{
[_window release];
[_splitViewController release];
[_rootViewController release];
[_detailViewController release];
[super dealloc];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSLog(#"CONNECTION: got auth challange");
NSString* message = [NSString stringWithFormat:#"CONNECTION: cred cout = %i", [[[NSURLCredentialStorage sharedCredentialStorage] allCredentials] count]];
NSLog(message);
NSLog([connection description]);
NSLog([NSString stringWithFormat:#"CONNECTION: host = %#", [[challenge protectionSpace] host]]);
NSLog([NSString stringWithFormat:#"CONNECTION: port = %i", [[challenge protectionSpace] port]]);
NSLog([NSString stringWithFormat:#"CONNECTION: protocol = %#", [[challenge protectionSpace] protocol]]);
NSLog([NSString stringWithFormat:#"CONNECTION: realm = %#", [[challenge protectionSpace] realm]]);
NSLog([NSString stringWithFormat:#"CONNECTION: authenticationMethod = %#", [[challenge protectionSpace] authenticationMethod]]);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
// release the connection, and the data object
[connection release];
// inform the user
NSLog(#"CONNECTION: failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
NSLog(#"CONNECTION: received response via nsurlconnection");
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
NSLog(#"CONNECTION: USE!");
return YES;
}
#end
The final solution for WebView authentication was based on custom protocol implementation. All protocols registered as a stack, so if you redefine HTTP protocol it would intercept all requests coming from webView, so you have to check attributes assotiated with incoming request and repack it into new request and send it again via your own connection. Since you are in stack, your request immidiatly comes to you again and you have to ignore it. So it goes down protocol stack to real HTTP protocol implementation, since your request is not athenticated you'll get authenticaiton request. And after authenticaiton you'll get a real response from server, so you repack response and reply to original request received from webView and that's it.
Don;t try to create new requests or responses bodies, you have to just resend them. The final code would be aproximetly 30-40 lines of code and it is quite simple, but requires a lot of debuging and tetsing.
Unfortunatlly I cannot provide code here, since I am assigned to different project already, I just wanted to say that my post is wrong way, it stucks when user changes password.
The secret to HTTP basic authentication using cocoa is knowing NSURL and the related classes.
NSURL
NSURLRequest/NSMutableURLRequest
NSURLConnection
NSURLCredential
NSURLCredentialStorage
NSURLProtectionSpace
UIWebView/WebView/NIWebController etc.
The real magic comes from NSURLConnection. In the words of the devDocs, "An NSURLConnection object provides support to perform the loading of a URL request." If you want to load some a URL in the background without displaying it you would use NSURLConnection. The real power of the NSURLConnection is in the method
+ (NSURLConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id < NSURLConnectionDelegate >)delegate
The NSURLConnectionDelegate protocol has methods for responding to successful connections, fatal errors, and authentication challenges. If you are trying to access data Protected by HTTP basic authentication this is how Cocoa does it. At this point an example should bring some clarity.
//basic HTTP authentication
NSURL *url = [NSURL URLWithString: urlString];
NSMutableURLRequest *request;
request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:12];
[self.webView openRequest:request];
(void)[NSURLConnection connectionWithRequest:request delegate:self];
This creates a URL. From the URL a URLRequest is created. The URLRequest is then loaded in the web view. The Request is also used to make a URLConnection. We don't really use the connection, but we need to receive notifications about authentication so we set the delegate. There are only two methods we need from the delegate.
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
NSURLCredential * cred = [NSURLCredential credentialWithUser:#"username"
password:#"password"
persistence:NSURLCredentialPersistenceForSession];
[[NSURLCredentialStorage sharedCredentialStorage]setCredential:cred forProtectionSpace:[challenge protectionSpace]];
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
return YES;
}
Whenever there is an authentication challenge a credential is added to the credential storage. You also tell the connection to use the credential storage.
I've just implemented this by setting basic auth credentials using an NSMutableURLRequest for the UIWebView. This also avoids the round trip incurred when implementing sharedCredentialStorage (of course there are tradeoffs involved).
Solution:
NSString *url = #"http://www.my-url-which-requires-basic-auth.io"
NSString *authStr = [NSString stringWithFormat:#"%#:%#", username, password];
NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
NSString *authValue = [NSString stringWithFormat:#"Basic %#", [authData base64EncodedString]];
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
[mutableRequest setValue:authValue forHTTPHeaderField:#"Authorization"];
NSURLRequest *request = [mutableRequest copy];
NSURLRequest *request = [NSURLRequest basicAuthHTTPURLRequestForUrl:url];
[self.webView loadRequest:request];
You can grab the NSData+Base64 category which implements the base64EncodedString for NSData from Matt Gallagher's page (it was at the bottom of the blog post when I downloaded it)
For TKAURLProtocolPro [http://kadao.dir.bg/cocoa.htm]
For SVWebViewController [https://github.com/samvermette/SVWebViewController]
Make sure to remember that logging out is not so easy with sessions and UIWebView credentials. See answer here: https://stackoverflow.com/a/18143902/2116338.