UIWebView para ver sitios web autofirmados (Sin API privada, no NSURLConnection) – ¿Es posible?

Hay un montón de preguntas que preguntan esto: ¿Puedo get UIWebView para ver un website HTTPS UIWebView ?

Y las respuestas siempre implican:

  1. Utilice la llamada api privada para NSURLRequest : allowsAnyHTTPSCertificateForHost
  2. Use NSURLConnection en NSURLConnection lugar y el delegado canAuthenticateAgainstProtectionSpace etc.

Para mí, estos no harán.
(1): significa que no puedo enviar a la tienda de aplicaciones con éxito.
(2): el uso de NSURLConnection significa que no se cargan las CSS, las imágenes y otras cosas que se deben get del server después de recibir la página HTML inicial.

¿Alguien sabe cómo usar UIWebView para ver una página web https autofirmada, que no implica los dos methods anteriores?

O bien, si se usa NSURLConnection se puede utilizar para representar una página web completa con CSS, imágenes y todo lo demás, ¡eso sería genial!

Aclamaciones,
Tramo.

¡Finalmente lo tengo!

Lo que puedes hacer es esto:

Inicie su request utilizando UIWebView manera normal. Luego, en webView:shouldStartLoadWithRequest , respondemos NO y, en su lugar, iniciaremos una NSURLConnection con la misma request.

Usando NSURLConnection , puede comunicarse con un server NSURLConnection , ya que tenemos la capacidad de controlar la authentication mediante los methods de delegado adicionales que no están disponibles para UIWebView . Entonces, usando la connection:didReceiveAuthenticationChallenge podemos autenticarnos contra el server connection:didReceiveAuthenticationChallenge .

Luego, en connection:didReceiveData , cancelamos la request de NSURLConnection e NSURLConnection la misma request nuevamente usando UIWebView , que funcionará ahora, porque ya hemos superado la authentication del server 🙂

Aquí están los fragments de código relevantes a continuación.

Nota: las variables de instancia que verá son del siguiente tipo:
UIWebView *_web
NSURLConnection *_urlConnection
NSURLRequest *_request

(Yo uso una var de instancia para _request ya que en mi caso es un POST con muchos detalles de inicio de session, pero podrías cambiar para usar la request que se pasó como arguments a los methods si lo necesitas.)

 #pragma mark - Webview delegate // Note: This method is particularly important. As the server is using a self signed certificate, // we cannot use just UIWebView - as it doesn't allow for using self-certs. Instead, we stop the // request in this method below, create an NSURLConnection (which can allow self-certs via the delegate methods // which UIWebView does not have), authenticate using NSURLConnection, then use another UIWebView to complete // the loading and viewing of the page. See connection:didReceiveAuthenticationChallenge to see how this works. - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType; { NSLog(@"Did start loading: %@ auth:%d", [[request URL] absoluteString], _authenticated); if (!_authenticated) { _authenticated = NO; _urlConnection = [[NSURLConnection alloc] initWithRequest:_request delegate:self]; [_urlConnection start]; return NO; } return YES; } #pragma mark - NURLConnection delegate - (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; { NSLog(@"WebController Got auth challange via NSURLConnection"); if ([challenge previousFailureCount] == 0) { _authenticated = YES; NSURLCnetworkingential *cnetworkingential = [NSURLCnetworkingential cnetworkingentialForTrust:challenge.protectionSpace.serverTrust]; [challenge.sender useCnetworkingential:cnetworkingential forAuthenticationChallenge:challenge]; } else { [[challenge sender] cancelAuthenticationChallenge:challenge]; } } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; { NSLog(@"WebController received response via NSURLConnection"); // remake a webview call now that authentication has passed ok. _authenticated = YES; [_web loadRequest:_request]; // Cancel the URL connection otherwise we double up (webview + url connection, same url = no good!) [_urlConnection cancel]; } // We use this method is to accept an untrusted site which unfortunately we need to do, as our PVM servers are self signed. - (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace { return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]; } 

¡Espero que esto ayude a otros con el mismo problema que tenía!

La respuesta de Stretch parece ser una gran solución, pero usa API desaprobadas. Por lo tanto, pensé que podría ser digno de una actualización del código.

Para este ejemplo de código, agregué las rutinas al ViewController que contiene mi UIWebView. Hice de mi UIViewController un UIWebViewDelegate y un NSURLConnectionDataDelegate. Luego agregué 2 miembros de datos: _Authenticated y _FailedRequest. Con eso, el código se ve así:

 -(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { BOOL result = _Authenticated; if (!_Authenticated) { _FailedRequest = request; [[NSURLConnection alloc] initWithRequest:request delegate:self]; } return result; } -(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { NSURL* baseURL = [_FailedRequest URL]; if ([challenge.protectionSpace.host isEqualToString:baseURL.host]) { NSLog(@"trusting connection to host %@", challenge.protectionSpace.host); [challenge.sender useCnetworkingential:[NSURLCnetworkingential cnetworkingentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge]; } else NSLog(@"Not trusting connection to host %@", challenge.protectionSpace.host); } [challenge.sender continueWithoutCnetworkingentialForAuthenticationChallenge:challenge]; } -(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)pResponse { _Authenticated = YES; [connection cancel]; [_WebView loadRequest:_FailedRequest]; } 

Configuro _Authenticated a NO cuando carga la vista y no la restablece. Esto parece permitir que UIWebView realice varias requestes al mismo sitio. No intenté cambiar de sitio y tratar de volver. Eso puede ocasionar la necesidad de restablecer _Autenticado. Además, si está cambiando sitios, debe mantener un dictionary (una input para cada host) para _Autenticado en lugar de un BOOL.

¡Esta es la panacea!


 BOOL _Authenticated; NSURLRequest *_FailedRequest; #pragma UIWebViewDelegate -(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { BOOL result = _Authenticated; if (!_Authenticated) { _FailedRequest = request; NSURLConnection *urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; [urlConnection start]; } return result; } #pragma NSURLConnectionDelegate -(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { NSURL* baseURL = [NSURL URLWithString:@"your url"]; if ([challenge.protectionSpace.host isEqualToString:baseURL.host]) { NSLog(@"trusting connection to host %@", challenge.protectionSpace.host); [challenge.sender useCnetworkingential:[NSURLCnetworkingential cnetworkingentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge]; } else NSLog(@"Not trusting connection to host %@", challenge.protectionSpace.host); } [challenge.sender continueWithoutCnetworkingentialForAuthenticationChallenge:challenge]; } -(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)pResponse { _Authenticated = YES; [connection cancel]; [self.webView loadRequest:_FailedRequest]; } - (void)viewDidLoad{ [super viewDidLoad]; NSURL *url = [NSURL URLWithString:@"your url"]; NSURLRequest *requestURL = [NSURLRequest requestWithURL:url]; [self.webView loadRequest:requestURL]; // Do any additional setup after loading the view. } 

Si desea acceder a un server privado con un certificate autofirmado solo para probar, no tiene que escribir código. Puede hacer manualmente una import de todo el sistema del certificate.

Para hacer esto, debe download el certificate del server con safari mobile, que luego solicita una import.

Esto sería útil en las siguientes circunstancias:

  • el número de dispositivos de testing es pequeño
  • estás confiando en el certificate del server

Si no tiene acceso al certificate del server, puede recurrir al siguiente método para extraerlo de cualquier server HTTPS (al less en Linux / Mac, los chicos de Windows tendrán que download un binary de OpenSSL en alguna parte):

 echo "" | openssl s_client -connect $server:$port -prexit 2>/dev/null | sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' >server.pem 

Tenga en count que, dependiendo de la versión de OpenSSL, el certificate puede duplicarse en el file, así que es mejor echarle un vistazo con un editor de text. Coloque el file en alguna parte de la networking o use el

python -m SimpleHTTPServer 8000

acceso directo para acceder a él desde su Safari mobile en http: // $ your_device_ip: 8000 / server.pem.

Esta es una solución inteligente. Sin embargo, una solución posiblemente mejor (aunque más intensiva en código) sería utilizar un NSURLProtocol como se demuestra en el código de ejemplo CustomHTTPProtocol de Apple. Desde el file README:

"CustomHTTPProtocol muestra cómo utilizar una subclass NSURLProtocol para interceptar las conexiones NSUR realizadas por un subsistema de alto nivel que de otro modo no expone sus conexiones de networking. En este caso específico, intercepta las requestes HTTPS realizadas por una vista web y anula la evaluación de la confianza del server, permitiéndole explorar un sitio cuyo certificate no es de confianza por defecto ".

Complete el ejemplo completo: https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/Introduction/Intro.html

Este es un equivalente compatible con Swift 2.0 que funciona para mí. No he convertido este código para usar NSURLSession lugar de NSURLConnection , y sospecho que agregaría mucha complejidad para hacerlo bien.

 var authRequest : NSURLRequest? = nil var authenticated = false var trustedDomains = [:] // set up as necessary func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool { if !authenticated { authRequest = request let urlConnection: NSURLConnection = NSURLConnection(request: request, delegate: self)! urlConnection.start() return false } else if isWebContent(request.URL!) { // write your method for this return true } return processData(request) // write your method for this } func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) { if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { let challengeHost = challenge.protectionSpace.host if let _ = trustedDomains[challengeHost] { challenge.sender!.useCnetworkingential(NSURLCnetworkingential(forTrust: challenge.protectionSpace.serverTrust!), forAuthenticationChallenge: challenge) } } challenge.sender!.continueWithoutCnetworkingentialForAuthenticationChallenge(challenge) } func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) { authenticated = true connection.cancel() webview!.loadRequest(authRequest!) } 

Aquí el código de trabajo de swift 2.0

 var authRequest : NSURLRequest? = nil var authenticated = false func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool { if !authenticated { authRequest = request let urlConnection: NSURLConnection = NSURLConnection(request: request, delegate: self)! urlConnection.start() return false } return true } func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) { authenticated = true connection.cancel() webView!.loadRequest(authRequest!) } func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) { let host = "www.example.com" if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust && challenge.protectionSpace.host == host { let cnetworkingential = NSURLCnetworkingential(forTrust: challenge.protectionSpace.serverTrust!) challenge.sender!.useCnetworkingential(cnetworkingential, forAuthenticationChallenge: challenge) } else { challenge.sender!.performDefaultHandlingForAuthenticationChallenge!(challenge) } } 

Para build la respuesta de @ spirographer , coloqué algo para un caso de uso de Swift 2.0 con NSURLSession . Sin embargo, esto todavía no funciona. Vea más abajo.

 func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool { let result = _Authenticated if !result { let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration() let session = NSURLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: NSOperationQueue.mainQueue()) let task = session.dataTaskWithRequest(request) { (data, response, error) -> Void in if error == nil { if (!self._Authenticated) { self._Authenticated = true; let pageData = NSString(data: data!, encoding: NSUTF8StringEncoding) self.webView.loadHTMLString(pageData as! String, baseURL: request.URL!) } else { self.webView.loadRequest(request) } } } task.resume() return false } return result } func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCnetworkingential?) -> Void) { completionHandler(NSURLSessionAuthChallengeDisposition.UseCnetworkingential, NSURLCnetworkingential(forTrust: challenge.protectionSpace.serverTrust!)) } 

Volveré la respuesta HTML inicial, por lo que la página muestra el HTML simple, pero no hay styles CSS aplicados (parece que la request para get CSS está denegada). Veo un montón de estos errores:

 NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 

Parece que cualquier request realizada con webView.loadRequest se realiza dentro de la session, por lo que se rechaza la connection. Tengo Allow Arbitrary Loads configuradas en Info.plist . Lo que me confunde es por qué NSURLConnection funcionaría (aparentemente la misma idea), pero no NSURLSession .