UIWebView para ver sitios web autofirmados (No api privada, no NSURLConnection) – ¿es posible?

Hay una gran cantidad de preguntas que hacen esto: ¿Puedo obtener UIWebView para ver un sitio web HTTPS UIWebView ?

Y las respuestas siempre involucran:

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

Para mí, esto no servirá.
(1): significa que no puedo enviarme a la tienda de aplicaciones con éxito.
(2) – usar NSURLConnection significa que el CSS, las imágenes y otras cosas que deben ser extraídas del servidor después de recibir la página HTML inicial no se cargan.

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

O bien, si utiliza NSURLConnection puede usarse para renderizar una página web completa con CSS, imágenes y todo lo demás, ¡sería genial!

Aclamaciones,
Tramo.

¡Finalmente lo tengo!

Lo que puedes hacer es esto:

Inicie su solicitud utilizando UIWebView como de costumbre. Luego, en webView:shouldStartLoadWithRequest , respondemos NO , y en su lugar iniciamos una NSURLConnection con la misma solicitud.

Usando NSURLConnection , puede comunicarse con un servidor NSURLConnection , ya que tenemos la capacidad de controlar la autenticación a través de los métodos de delegado adicionales que no están disponibles para un UIWebView . Entonces, usando connection:didReceiveAuthenticationChallenge podemos autenticar contra el servidor connection:didReceiveAuthenticationChallenge .

Luego, en connection:didReceiveData , cancelamos la solicitud de NSURLConnection , y comenzamos la misma solicitud nuevamente usando UIWebView , que funcionará ahora, porque ya hemos obtenido la autenticación del servidor 🙂

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

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

(Utilizo una instancia de var para _request ya que en mi caso es un POST con muchos detalles de inicio de sesión, pero puede cambiar para usar la solicitud pasada como argumentos a los métodos si lo necesita).

 #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; NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; [challenge.sender useCredential:credential 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 estaba teniendo!

La respuesta de Stretch parece ser una gran solución, pero usa API obsoletas. Entonces, pensé que sería digno de una actualización del código.

Para este ejemplo de código, agregué las rutinas al ViewController que contiene mi UIWebView. Hice 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 useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge]; } else NSLog(@"Not trusting connection to host %@", challenge.protectionSpace.host); } [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge]; } -(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)pResponse { _Authenticated = YES; [connection cancel]; [_WebView loadRequest:_FailedRequest]; } 

Configuré _Authenticated a NO cuando cargo la vista y no la reinicio. Esto parece permitir que UIWebView realice múltiples solicitudes al mismo sitio. No intenté cambiar de sitio e intentar regresar. Eso puede causar la necesidad de reiniciar _Authenticated. Además, si cambia de sitio, debe mantener un diccionario (una entrada para cada host) para _Authenticated 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 useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge]; } else NSLog(@"Not trusting connection to host %@", challenge.protectionSpace.host); } [challenge.sender continueWithoutCredentialForAuthenticationChallenge: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 servidor privado con un certificado autofirmado solo para realizar pruebas, no es necesario que escriba el código. Puede realizar manualmente una importación de todo el sistema del certificado.

Para hacerlo, debe descargar el certificado del servidor con Safari móvil, que luego solicita una importación.

Esto sería utilizable bajo las siguientes circunstancias:

  • la cantidad de dispositivos de prueba es pequeña
  • estás confiando en el certificado del servidor

Si no tiene acceso al certificado del servidor, puede recurrir al siguiente método para extraerlo de cualquier servidor HTTPS (al menos en Linux / Mac, Windows tendrá que descargar un binario 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 cuenta que, dependiendo de la versión de OpenSSL, el certificado puede duplicarse en el archivo, por lo que es mejor echarle un vistazo con un editor de texto. Coloque el archivo en algún lugar de la red o use el

python -m SimpleHTTPServer 8000

acceso directo para acceder desde su safari móvil 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 cuanto a los códigos) sería utilizar un protocolo NSURL como se demuestra en el código de muestra CustomHTTPProtocol de Apple. Del LÉAME:

“CustomHTTPProtocol muestra cómo usar una subclase NSURLProtocol para interceptar las NSURLConnections realizadas por un subsistema de alto nivel que no expone sus conexiones de red. En este caso específico, intercepta las solicitudes HTTPS realizadas por una vista web y anula la evaluación de confianza del servidor. permitiéndote navegar por un sitio cuyo certificado no es de confianza por defecto “.

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

Este es un equivalente rápido 2.0 compatible 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!.useCredential(NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!), forAuthenticationChallenge: challenge) } } challenge.sender!.continueWithoutCredentialForAuthenticationChallenge(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 credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!) challenge.sender!.useCredential(credential, forAuthenticationChallenge: challenge) } else { challenge.sender!.performDefaultHandlingForAuthenticationChallenge!(challenge) } } 

Para construir la respuesta de @ spirographer , NSURLSession algo para un caso de uso de Swift 2.0 con NSURLSession . Sin embargo, esto aún NO está funcionando. Ver 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, NSURLCredential?) -> Void) { completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)) } 

Volveré a obtener la respuesta HTML inicial, por lo que la página representa el HTML simple, pero no se le aplica ningún estilo CSS (parece que se deniega la solicitud para obtener CSS). Veo un montón de estos errores:

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

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