web-dev-qa-db-fra.com

UIWebView pour afficher des sites Web auto-signés (pas d'API privée, pas de NSURLConnection) - est-ce possible?

Il y a beaucoup de questions qui demandent ceci: Puis-je obtenir UIWebView pour afficher un site Web HTTPS auto-signé?

Et les réponses impliquent toujours soit:

  1. Utilisez l'appel privé de l'API pour NSURLRequest: allowsAnyHTTPSCertificateForHost
  2. Utilisez plutôt NSURLConnection et le délégué canAuthenticateAgainstProtectionSpace etc.

Pour moi, ça ne va pas.
(1) - signifie que je ne peux pas soumettre à l'App Store avec succès.
(2) - utilisation de NSURLConnection signifie que la feuille CSS, les images et autres éléments à extraire du serveur après la réception de la page HTML initiale ne sont pas chargés.

Quelqu'un sait-il comment utiliser UIWebView pour afficher une page Web auto-signée https, qui n'implique pas les deux méthodes ci-dessus?

Ou - Si vous utilisez NSURLConnection, vous pouvez en fait utiliser le rendu d'une page Web avec CSS, des images et tout le reste - ce serait génial!

À votre santé,
Étendue.

49
Stretch

Je l'ai finalement eu!

Voici ce que vous pouvez faire:

Initiez votre demande en utilisant UIWebView comme d'habitude. Puis - dans webView:shouldStartLoadWithRequest - nous répondonsNOet commençons plutôt une connexion NSURLC avec la même demande. 

En utilisant NSURLConnection, vous pouvez communiquer avec un serveur auto-signé, car nous avons la possibilité de contrôler l’authentification par le biais des méthodes de délégation supplémentaires qui ne sont pas disponibles pour UIWebView. Donc, en utilisant connection:didReceiveAuthenticationChallenge, nous pouvons nous authentifier auprès du serveur auto-signé. 

Ensuite, dans connection:didReceiveData, nous annulons la demande NSURLConnection et recommençons la même demande en utilisant UIWebView - ce qui fonctionnera maintenant car nous avons déjà effectué l'authentification du serveur :)

Voici les extraits de code pertinents ci-dessous.

Remarque: les variables d'instance que vous verrez sont du type suivant: 
UIWebView *_web
NSURLConnection *_urlConnection
NSURLRequest *_request

(J'utilise une instance var pour _request car, dans mon cas, il s'agit d'un POST] contenant de nombreux détails de connexion, mais vous pouvez changer pour utiliser la demande transmise en tant qu'argument aux méthodes, le cas échéant.

#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];
}

J'espère que cela aide les autres à avoir le même problème que moi!

75
Stretch

La réponse de Stretch semble être une excellente solution de contournement, mais elle utilise des API obsolètes. J'ai donc pensé que cela mériterait une mise à jour du code.

Pour cet exemple de code, j'ai ajouté les routines au ViewController qui contient mon UIWebView. J'ai fait de mon UIViewController un UIWebViewDelegate et un NSURLConnectionDataDelegate. Ensuite, j'ai ajouté 2 membres de données: _Authenticated et _FailedRequest. Avec cela, le code ressemble à ceci:

-(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];
}

J'ai défini _Authenticated sur NO lorsque je charge la vue et je ne la réinitialise pas. Cela semble permettre à UIWebView de faire plusieurs demandes sur le même site. Je n'ai pas essayé de changer de site et d'essayer de revenir. Cela peut entraîner la nécessité de réinitialiser _Authenticated. De plus, si vous changez de site, vous devez conserver un dictionnaire (une entrée pour chaque hôte) pour _Authenticated au lieu d'un BOOL.

64
Prof Von Lemongargle

C'est la panacée!


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.
}
16
Wilson Aguiar

Si vous souhaitez accéder à un serveur privé avec un certificat auto-signé juste pour le test, vous n'avez pas à écrire de code. Vous pouvez manuellement importer le certificat dans tout le système. 

Pour ce faire, vous devez télécharger le certificat de serveur avec Mobile Safari, qui demande ensuite une importation. 

Cela serait utilisable dans les circonstances suivantes:

  • le nombre de dispositifs de test est petit
  • vous faites confiance au certificat du serveur

Si vous n'avez pas accès au certificat de serveur, vous pouvez utiliser la méthode suivante pour l'extraire de n'importe quel serveur HTTPS (au moins sous Linux/Mac, les utilisateurs Windows devront télécharger un binaire OpenSSL quelque part) :

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

Notez que, selon la version de OpenSSL, le certificat peut être doublé dans le fichier, il est donc préférable de l'examiner avec un éditeur de texte. Placez le fichier quelque part sur le réseau ou utilisez la commande 

python -m SimpleHTTPServer 8000

raccourci pour y accéder depuis votre safari mobile à l’adresse http: // $ your_device_ip: 8000/server.pem.

7
zliw

Ceci est une solution de contournement intelligente. Cependant, une solution éventuellement meilleure (bien que nécessitant davantage de code) consisterait à utiliser un protocole NSURLProtocol, comme illustré dans l'exemple de code CustomHTTPProtocol d'Apple. Du README:

"CustomHTTPProtocol montre comment utiliser une sous-classe NSURLProtocol pour intercepter les connexions NSURLC connectées par un sous-système de haut niveau qui n'expose pas par ailleurs ses connexions réseau. Dans ce cas spécifique, il intercepte les demandes HTTPS effectuées par une vue Web et annule l'évaluation de la confiance du serveur vous permettant de naviguer sur un site dont le certificat n'est pas approuvé par défaut. "

Découvrez l'exemple complet: https://developer.Apple.com/library/ios/samplecode/CustomHTTPProtocol/Introduction/Intro.html

4
Alex

Ceci est un équivalent compatible Swift 2.0 qui fonctionne pour moi. Je n'ai pas converti ce code pour qu'il utilise NSURLSession au lieu de NSURLConnection et je soupçonne que cela compliquerait considérablement la tâche.

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!)
}
3
spirographer

Voici le code de travail 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)
                }
}
2
Velu Loganathan

Pour construire à partir de @ spirographer's answer , j'ai assemblé quelque chose pour un cas d'utilisation de Swift 2.0 avec NSURLSession. Cependant, cela fonctionne toujours PAS. Voir plus ci-dessous.

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!))
}

Je vais récupérer la réponse HTML initiale, donc la page restitue le code HTML brut, mais aucun style CSS n'y est appliqué (la requête d'obtention de CSS est apparemment refusée). Je vois un tas de ces erreurs:

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

Il semble que toute requête faite avec webView.loadRequest ne soit pas faite dans la session, ce qui explique pourquoi la connexion est rejetée. J'ai Allow Arbitrary Loads défini dans Info.plist. Ce qui me trouble, c'est pourquoi NSURLConnection fonctionnerait (apparemment la même idée), mais pas NSURLSession.

0
Tri Nguyen

La première chose UIWebView est obsolète

utilisez WKWebView à la place (disponible sur iOS8)

set webView.navigationDelegate = self

mettre en place

extension ViewController: WKNavigationDelegate {

func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    let trust = challenge.protectionSpace.serverTrust!
    let exceptions = SecTrustCopyExceptions(trust)
    SecTrustSetExceptions(trust, exceptions)
        completionHandler(.useCredential, URLCredential(trust: trust))
    }

}

Et ajoutez ceci dans la liste avec les domaines que vous souhaitez autoriser

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSTemporaryExceptionAllowsInsecureHTTPSLoads</key>
            <false/>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
            <true/>
            <key>NSTemporaryExceptionMinimumTLSVersion</key>
            <string>1.0</string>
            <key>NSTemporaryExceptionRequiresForwardSecrecy</key>
            <false/>
        </dict>
    </dict>
</dict>
0
Yatheesha B L