Support manually accepting invalid SSL certificates with NetworkSession
[WebKit-https.git] / Source / WebKit2 / NetworkProcess / cocoa / NetworkDataTaskCocoa.mm
1 /*
2  * Copyright (C) 2016 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "NetworkSession.h"
28
29 #if USE(NETWORK_SESSION)
30
31 #import "Download.h"
32 #import "DownloadProxyMessages.h"
33 #import "NetworkProcess.h"
34 #import "SessionTracker.h"
35 #import "WebCoreArgumentCoders.h"
36 #import <WebCore/AuthenticationChallenge.h>
37 #import <WebCore/CFNetworkSPI.h>
38 #import <WebCore/NetworkStorageSession.h>
39 #import <WebCore/ResourceRequest.h>
40 #import <wtf/MainThread.h>
41 #import <wtf/text/Base64.h>
42
43 namespace WebKit {
44 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
45 static void applyBasicAuthorizationHeader(WebCore::ResourceRequest& request, const WebCore::Credential& credential)
46 {
47     String authenticationHeader = "Basic " + base64Encode(String(credential.user() + ":" + credential.password()).utf8());
48     request.setHTTPHeaderField(WebCore::HTTPHeaderName::Authorization, authenticationHeader);
49 }
50 #endif
51
52 NetworkDataTask::NetworkDataTask(NetworkSession& session, NetworkDataTaskClient& client, const WebCore::ResourceRequest& requestWithCredentials, WebCore::StoredCredentials storedCredentials, WebCore::ContentSniffingPolicy shouldContentSniff, bool shouldClearReferrerOnHTTPSToHTTPRedirect)
53     : m_failureTimer(*this, &NetworkDataTask::failureTimerFired)
54     , m_session(&session)
55     , m_client(&client)
56     , m_storedCredentials(storedCredentials)
57     , m_lastHTTPMethod(requestWithCredentials.httpMethod())
58     , m_firstRequest(requestWithCredentials)
59     , m_shouldClearReferrerOnHTTPSToHTTPRedirect(shouldClearReferrerOnHTTPSToHTTPRedirect)
60 {
61     ASSERT(isMainThread());
62     
63     if (!requestWithCredentials.url().isValid()) {
64         scheduleFailure(InvalidURLFailure);
65         return;
66     }
67     
68     if (!portAllowed(requestWithCredentials.url())) {
69         scheduleFailure(BlockedFailure);
70         return;
71     }
72     
73     auto request = requestWithCredentials;
74     auto url = request.url();
75     if (storedCredentials == WebCore::AllowStoredCredentials && url.protocolIsInHTTPFamily()) {
76         m_user = url.user();
77         m_password = url.pass();
78         request.removeCredentials();
79         url = request.url();
80     
81 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
82         if (m_user.isEmpty() && m_password.isEmpty())
83             m_initialCredential = m_session->networkStorageSession().credentialStorage().get(url);
84         else
85             m_session->networkStorageSession().credentialStorage().set(WebCore::Credential(m_user, m_password, WebCore::CredentialPersistenceNone), url);
86 #endif
87     }
88
89 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
90     if (!m_initialCredential.isEmpty()) {
91         // FIXME: Support Digest authentication, and Proxy-Authorization.
92         applyBasicAuthorizationHeader(request, m_initialCredential);
93     }
94 #endif
95     
96     NSURLRequest *nsRequest = request.nsURLRequest(WebCore::UpdateHTTPBody);
97     if (shouldContentSniff == WebCore::DoNotSniffContent || url.protocolIs("file")) {
98         NSMutableURLRequest *mutableRequest = [[nsRequest mutableCopy] autorelease];
99         [mutableRequest _setProperty:@(NO) forKey:(NSString *)_kCFURLConnectionPropertyShouldSniff];
100         nsRequest = mutableRequest;
101     }
102
103     if (storedCredentials == WebCore::AllowStoredCredentials) {
104         m_task = [m_session->m_sessionWithCredentialStorage dataTaskWithRequest:nsRequest];
105         ASSERT(!m_session->m_dataTaskMapWithCredentials.contains([m_task taskIdentifier]));
106         m_session->m_dataTaskMapWithCredentials.add([m_task taskIdentifier], this);
107     } else {
108         m_task = [m_session->m_sessionWithoutCredentialStorage dataTaskWithRequest:nsRequest];
109         ASSERT(!m_session->m_dataTaskMapWithoutCredentials.contains([m_task taskIdentifier]));
110         m_session->m_dataTaskMapWithoutCredentials.add([m_task taskIdentifier], this);
111     }
112
113 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
114     String storagePartition = WebCore::cookieStoragePartition(request);
115     if (!storagePartition.isEmpty())
116         m_task.get()._storagePartitionIdentifier = storagePartition;
117 #endif
118 }
119
120 NetworkDataTask::~NetworkDataTask()
121 {
122     ASSERT(isMainThread());
123     if (m_task) {
124         if (m_storedCredentials == WebCore::StoredCredentials::AllowStoredCredentials) {
125             ASSERT(m_session->m_dataTaskMapWithCredentials.get([m_task taskIdentifier]) == this);
126             m_session->m_dataTaskMapWithCredentials.remove([m_task taskIdentifier]);
127         } else {
128             ASSERT(m_session->m_dataTaskMapWithoutCredentials.get([m_task taskIdentifier]) == this);
129             m_session->m_dataTaskMapWithoutCredentials.remove([m_task taskIdentifier]);
130         }
131     }
132 }
133
134 void NetworkDataTask::didSendData(uint64_t totalBytesSent, uint64_t totalBytesExpectedToSend)
135 {
136     if (m_client)
137         m_client->didSendData(totalBytesSent, totalBytesExpectedToSend);
138 }
139
140 void NetworkDataTask::didReceiveChallenge(const WebCore::AuthenticationChallenge& challenge, ChallengeCompletionHandler completionHandler)
141 {
142     // Proxy authentication is handled by CFNetwork internally. We can get here if the user cancels
143     // CFNetwork authentication dialog, and we shouldn't ask the client to display another one in that case.
144     if (challenge.protectionSpace().isProxy()) {
145         completionHandler(AuthenticationChallengeDisposition::UseCredential, { });
146         return;
147     }
148
149     if (tryPasswordBasedAuthentication(challenge, completionHandler))
150         return;
151
152     if (m_client)
153         m_client->didReceiveChallenge(challenge, completionHandler);
154     else {
155         ASSERT_NOT_REACHED();
156         completionHandler(AuthenticationChallengeDisposition::PerformDefaultHandling, { });
157     }
158 }
159
160 void NetworkDataTask::didCompleteWithError(const WebCore::ResourceError& error)
161 {
162     if (m_client)
163         m_client->didCompleteWithError(error);
164 }
165
166 void NetworkDataTask::didReceiveResponse(const WebCore::ResourceResponse& response, ResponseCompletionHandler completionHandler)
167 {
168     if (m_client)
169         m_client->didReceiveResponseNetworkSession(response, completionHandler);
170     else {
171         ASSERT_NOT_REACHED();
172         completionHandler(WebCore::PolicyAction::PolicyIgnore);
173     }
174 }
175
176 void NetworkDataTask::didReceiveData(RefPtr<WebCore::SharedBuffer>&& data)
177 {
178     if (m_client)
179         m_client->didReceiveData(WTFMove(data));
180 }
181
182 void NetworkDataTask::didBecomeDownload()
183 {
184     if (m_client)
185         m_client->didBecomeDownload();
186 }
187
188 void NetworkDataTask::willPerformHTTPRedirection(const WebCore::ResourceResponse& redirectResponse, WebCore::ResourceRequest&& request, RedirectCompletionHandler completionHandler)
189 {
190     if (redirectResponse.httpStatusCode() == 307 || redirectResponse.httpStatusCode() == 308) {
191         ASSERT(m_lastHTTPMethod == request.httpMethod());
192         WebCore::FormData* body = m_firstRequest.httpBody();
193         if (body && !body->isEmpty() && !equalLettersIgnoringASCIICase(m_lastHTTPMethod, "get"))
194             request.setHTTPBody(body);
195         
196         String originalContentType = m_firstRequest.httpContentType();
197         if (!originalContentType.isEmpty())
198             request.setHTTPHeaderField(WebCore::HTTPHeaderName::ContentType, originalContentType);
199     }
200     
201     // Should not set Referer after a redirect from a secure resource to non-secure one.
202     if (m_shouldClearReferrerOnHTTPSToHTTPRedirect && !request.url().protocolIs("https") && WebCore::protocolIs(request.httpReferrer(), "https"))
203         request.clearHTTPReferrer();
204     
205     const auto& url = request.url();
206     m_user = url.user();
207     m_password = url.pass();
208     m_lastHTTPMethod = request.httpMethod();
209     request.removeCredentials();
210     
211     if (!protocolHostAndPortAreEqual(request.url(), redirectResponse.url())) {
212         // The network layer might carry over some headers from the original request that
213         // we want to strip here because the redirect is cross-origin.
214         request.clearHTTPAuthorization();
215         request.clearHTTPOrigin();
216 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
217     } else {
218         // Only consider applying authentication credentials if this is actually a redirect and the redirect
219         // URL didn't include credentials of its own.
220         if (m_user.isEmpty() && m_password.isEmpty() && !redirectResponse.isNull()) {
221             auto credential = m_session->networkStorageSession().credentialStorage().get(request.url());
222             if (!credential.isEmpty()) {
223                 m_initialCredential = credential;
224
225                 // FIXME: Support Digest authentication, and Proxy-Authorization.
226                 applyBasicAuthorizationHeader(request, m_initialCredential);
227             }
228         }
229 #endif
230     }
231     
232     if (m_client)
233         m_client->willPerformHTTPRedirection(redirectResponse, request, completionHandler);
234     else {
235         ASSERT_NOT_REACHED();
236         completionHandler({ });
237     }
238 }
239     
240 void NetworkDataTask::scheduleFailure(FailureType type)
241 {
242     ASSERT(type != NoFailure);
243     m_scheduledFailureType = type;
244     m_failureTimer.startOneShot(0);
245 }
246
247 void NetworkDataTask::failureTimerFired()
248 {
249     RefPtr<NetworkDataTask> protect(this);
250     
251     switch (m_scheduledFailureType) {
252     case BlockedFailure:
253         m_scheduledFailureType = NoFailure;
254         if (m_client)
255             m_client->wasBlocked();
256         return;
257     case InvalidURLFailure:
258         m_scheduledFailureType = NoFailure;
259         if (m_client)
260             m_client->cannotShowURL();
261         return;
262     case NoFailure:
263         ASSERT_NOT_REACHED();
264         break;
265     }
266     ASSERT_NOT_REACHED();
267 }
268
269 void NetworkDataTask::setPendingDownloadLocation(const WTF::String& filename, const SandboxExtension::Handle& sandboxExtensionHandle)
270 {
271     ASSERT(!m_sandboxExtension);
272     m_sandboxExtension = SandboxExtension::create(sandboxExtensionHandle);
273     if (m_sandboxExtension)
274         m_sandboxExtension->consume();
275
276     m_pendingDownloadLocation = filename;
277     m_task.get()._pathToDownloadTaskFile = filename;
278 }
279
280 bool NetworkDataTask::tryPasswordBasedAuthentication(const WebCore::AuthenticationChallenge& challenge, ChallengeCompletionHandler completionHandler)
281 {
282     if (!challenge.protectionSpace().isPasswordBased())
283         return false;
284     
285     if (!m_user.isNull() && !m_password.isNull()) {
286         auto persistence = m_storedCredentials == WebCore::StoredCredentials::AllowStoredCredentials ? WebCore::CredentialPersistenceForSession : WebCore::CredentialPersistenceNone;
287         completionHandler(AuthenticationChallengeDisposition::UseCredential, WebCore::Credential(m_user, m_password, persistence));
288         m_user = String();
289         m_password = String();
290         return true;
291     }
292
293 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
294     if (m_storedCredentials == WebCore::AllowStoredCredentials) {
295         if (!m_initialCredential.isEmpty() || challenge.previousFailureCount()) {
296             // The stored credential wasn't accepted, stop using it.
297             // There is a race condition here, since a different credential might have already been stored by another ResourceHandle,
298             // but the observable effect should be very minor, if any.
299             m_session->networkStorageSession().credentialStorage().remove(challenge.protectionSpace());
300         }
301
302         if (!challenge.previousFailureCount()) {
303             auto credential = m_session->networkStorageSession().credentialStorage().get(challenge.protectionSpace());
304             if (!credential.isEmpty() && credential != m_initialCredential) {
305                 ASSERT(credential.persistence() == WebCore::CredentialPersistenceNone);
306                 if (challenge.failureResponse().httpStatusCode() == 401) {
307                     // Store the credential back, possibly adding it as a default for this directory.
308                     m_session->networkStorageSession().credentialStorage().set(credential, challenge.protectionSpace(), challenge.failureResponse().url());
309                 }
310                 completionHandler(AuthenticationChallengeDisposition::UseCredential, credential);
311                 return true;
312             }
313         }
314     }
315 #endif
316
317     if (!challenge.proposedCredential().isEmpty() && !challenge.previousFailureCount()) {
318         completionHandler(AuthenticationChallengeDisposition::UseCredential, challenge.proposedCredential());
319         return true;
320     }
321     
322     return false;
323 }
324
325 void NetworkDataTask::transferSandboxExtensionToDownload(Download& download)
326 {
327     download.setSandboxExtension(WTFMove(m_sandboxExtension));
328 }
329
330 static bool certificatesMatch(SecTrustRef trust1, SecTrustRef trust2)
331 {
332     if (!trust1 || !trust2)
333         return false;
334
335     CFIndex count1 = SecTrustGetCertificateCount(trust1);
336     CFIndex count2 = SecTrustGetCertificateCount(trust2);
337     if (count1 != count2)
338         return false;
339
340     for (CFIndex i = 0; i < count1; i++) {
341         if (!CFEqual(SecTrustGetCertificateAtIndex(trust1, i), SecTrustGetCertificateAtIndex(trust2, i)))
342             return false;
343     }
344
345     return true;
346 }
347
348 bool NetworkDataTask::allowsSpecificHTTPSCertificateForHost(const WebCore::AuthenticationChallenge& challenge)
349 {
350     const String& host = challenge.protectionSpace().host();
351     NSArray *certificates = [NSURLRequest allowsSpecificHTTPSCertificateForHost:host];
352     if (!certificates)
353         return false;
354     
355     bool requireServerCertificates = challenge.protectionSpace().authenticationScheme() == WebCore::ProtectionSpaceAuthenticationScheme::ProtectionSpaceAuthenticationSchemeServerTrustEvaluationRequested;
356     RetainPtr<SecPolicyRef> policy = adoptCF(SecPolicyCreateSSL(requireServerCertificates, host.createCFString().get()));
357     
358     SecTrustRef trustRef = nullptr;
359     if (SecTrustCreateWithCertificates((CFArrayRef)certificates, policy.get(), &trustRef) != noErr)
360         return false;
361     RetainPtr<SecTrustRef> trust = adoptCF(trustRef);
362
363     return certificatesMatch(trust.get(), challenge.nsURLAuthenticationChallenge().protectionSpace.serverTrust);
364 }
365
366 String NetworkDataTask::suggestedFilename()
367 {
368     return m_task.get().response.suggestedFilename;
369 }
370
371 WebCore::ResourceRequest NetworkDataTask::currentRequest()
372 {
373     return [m_task currentRequest];
374 }
375
376 void NetworkDataTask::cancel()
377 {
378     [m_task cancel];
379 }
380
381 void NetworkDataTask::resume()
382 {
383     if (m_scheduledFailureType != NoFailure)
384         m_failureTimer.startOneShot(0);
385     [m_task resume];
386 }
387
388 void NetworkDataTask::suspend()
389 {
390     if (m_failureTimer.isActive())
391         m_failureTimer.stop();
392     [m_task suspend];
393 }
394
395 WebCore::Credential serverTrustCredential(const WebCore::AuthenticationChallenge& challenge)
396 {
397     return WebCore::Credential([NSURLCredential credentialForTrust:challenge.nsURLAuthenticationChallenge().protectionSpace.serverTrust]);
398 }
399
400 }
401
402 #endif