[Resource Timing] Gather timing information with reliable responseEnd time
[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 "NetworkDataTaskCocoa.h"
28
29 #if USE(NETWORK_SESSION)
30
31 #import "AuthenticationManager.h"
32 #import "Download.h"
33 #import "DownloadProxyMessages.h"
34 #import "Logging.h"
35 #import "NetworkProcess.h"
36 #import "NetworkSessionCocoa.h"
37 #import "SessionTracker.h"
38 #import "WebCoreArgumentCoders.h"
39 #import <WebCore/AuthenticationChallenge.h>
40 #import <WebCore/CFNetworkSPI.h>
41 #import <WebCore/FileSystem.h>
42 #import <WebCore/NetworkStorageSession.h>
43 #import <WebCore/NotImplemented.h>
44 #import <WebCore/ResourceRequest.h>
45 #import <wtf/MainThread.h>
46 #import <wtf/text/Base64.h>
47
48 namespace WebKit {
49 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
50 static void applyBasicAuthorizationHeader(WebCore::ResourceRequest& request, const WebCore::Credential& credential)
51 {
52     String authenticationHeader = "Basic " + base64Encode(String(credential.user() + ":" + credential.password()).utf8());
53     request.setHTTPHeaderField(WebCore::HTTPHeaderName::Authorization, authenticationHeader);
54 }
55 #endif
56
57 NetworkDataTaskCocoa::NetworkDataTaskCocoa(NetworkSession& session, NetworkDataTaskClient& client, const WebCore::ResourceRequest& requestWithCredentials, WebCore::StoredCredentials storedCredentials, WebCore::ContentSniffingPolicy shouldContentSniff, bool shouldClearReferrerOnHTTPSToHTTPRedirect)
58     : NetworkDataTask(session, client, requestWithCredentials, storedCredentials, shouldClearReferrerOnHTTPSToHTTPRedirect)
59 {
60     if (m_scheduledFailureType != NoFailure)
61         return;
62
63     auto request = requestWithCredentials;
64     auto url = request.url();
65     if (storedCredentials == WebCore::AllowStoredCredentials && url.protocolIsInHTTPFamily()) {
66         m_user = url.user();
67         m_password = url.pass();
68         request.removeCredentials();
69         url = request.url();
70     
71 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
72         if (m_user.isEmpty() && m_password.isEmpty())
73             m_initialCredential = m_session->networkStorageSession().credentialStorage().get(m_partition, url);
74         else
75             m_session->networkStorageSession().credentialStorage().set(m_partition, WebCore::Credential(m_user, m_password, WebCore::CredentialPersistenceNone), url);
76 #endif
77     }
78
79 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
80     if (!m_initialCredential.isEmpty()) {
81         // FIXME: Support Digest authentication, and Proxy-Authorization.
82         applyBasicAuthorizationHeader(request, m_initialCredential);
83     }
84 #endif
85     
86     NSURLRequest *nsRequest = request.nsURLRequest(WebCore::UpdateHTTPBody);
87     if (shouldContentSniff == WebCore::DoNotSniffContent || url.protocolIs("file")) {
88         NSMutableURLRequest *mutableRequest = [[nsRequest mutableCopy] autorelease];
89         [mutableRequest _setProperty:@(NO) forKey:(NSString *)_kCFURLConnectionPropertyShouldSniff];
90         nsRequest = mutableRequest;
91     }
92
93     auto& cocoaSession = static_cast<NetworkSessionCocoa&>(m_session.get());
94     if (storedCredentials == WebCore::AllowStoredCredentials) {
95         m_task = [cocoaSession.m_sessionWithCredentialStorage dataTaskWithRequest:nsRequest];
96         ASSERT(!cocoaSession.m_dataTaskMapWithCredentials.contains([m_task taskIdentifier]));
97         cocoaSession.m_dataTaskMapWithCredentials.add([m_task taskIdentifier], this);
98     } else {
99         m_task = [cocoaSession.m_sessionWithoutCredentialStorage dataTaskWithRequest:nsRequest];
100         ASSERT(!cocoaSession.m_dataTaskMapWithoutCredentials.contains([m_task taskIdentifier]));
101         cocoaSession.m_dataTaskMapWithoutCredentials.add([m_task taskIdentifier], this);
102     }
103     LOG(NetworkSession, "%llu Creating NetworkDataTask with URL %s", [m_task taskIdentifier], nsRequest.URL.absoluteString.UTF8String);
104
105 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
106     String storagePartition = WebCore::cookieStoragePartition(request);
107     if (!storagePartition.isEmpty())
108         m_task.get()._storagePartitionIdentifier = storagePartition;
109 #endif
110 }
111
112 NetworkDataTaskCocoa::~NetworkDataTaskCocoa()
113 {
114     if (!m_task)
115         return;
116
117     auto& cocoaSession = static_cast<NetworkSessionCocoa&>(m_session.get());
118     if (m_storedCredentials == WebCore::StoredCredentials::AllowStoredCredentials) {
119         ASSERT(cocoaSession.m_dataTaskMapWithCredentials.get([m_task taskIdentifier]) == this);
120         cocoaSession.m_dataTaskMapWithCredentials.remove([m_task taskIdentifier]);
121     } else {
122         ASSERT(cocoaSession.m_dataTaskMapWithoutCredentials.get([m_task taskIdentifier]) == this);
123         cocoaSession.m_dataTaskMapWithoutCredentials.remove([m_task taskIdentifier]);
124     }
125 }
126
127 void NetworkDataTaskCocoa::didSendData(uint64_t totalBytesSent, uint64_t totalBytesExpectedToSend)
128 {
129     if (m_client)
130         m_client->didSendData(totalBytesSent, totalBytesExpectedToSend);
131 }
132
133 void NetworkDataTaskCocoa::didReceiveChallenge(const WebCore::AuthenticationChallenge& challenge, ChallengeCompletionHandler&& completionHandler)
134 {
135     // Proxy authentication is handled by CFNetwork internally. We can get here if the user cancels
136     // CFNetwork authentication dialog, and we shouldn't ask the client to display another one in that case.
137     if (challenge.protectionSpace().isProxy()) {
138         completionHandler(AuthenticationChallengeDisposition::UseCredential, { });
139         return;
140     }
141
142     if (tryPasswordBasedAuthentication(challenge, completionHandler))
143         return;
144
145     if (m_client)
146         m_client->didReceiveChallenge(challenge, WTFMove(completionHandler));
147     else {
148         ASSERT_NOT_REACHED();
149         completionHandler(AuthenticationChallengeDisposition::PerformDefaultHandling, { });
150     }
151 }
152
153 void NetworkDataTaskCocoa::didCompleteWithError(const WebCore::ResourceError& error, const WebCore::NetworkLoadMetrics& networkLoadMetrics)
154 {
155     if (m_client)
156         m_client->didCompleteWithError(error, networkLoadMetrics);
157 }
158
159 void NetworkDataTaskCocoa::didReceiveData(Ref<WebCore::SharedBuffer>&& data)
160 {
161     if (m_client)
162         m_client->didReceiveData(WTFMove(data));
163 }
164
165 void NetworkDataTaskCocoa::willPerformHTTPRedirection(WebCore::ResourceResponse&& redirectResponse, WebCore::ResourceRequest&& request, RedirectCompletionHandler&& completionHandler)
166 {
167     if (redirectResponse.httpStatusCode() == 307 || redirectResponse.httpStatusCode() == 308) {
168         ASSERT(m_lastHTTPMethod == request.httpMethod());
169         WebCore::FormData* body = m_firstRequest.httpBody();
170         if (body && !body->isEmpty() && !equalLettersIgnoringASCIICase(m_lastHTTPMethod, "get"))
171             request.setHTTPBody(body);
172         
173         String originalContentType = m_firstRequest.httpContentType();
174         if (!originalContentType.isEmpty())
175             request.setHTTPHeaderField(WebCore::HTTPHeaderName::ContentType, originalContentType);
176     }
177     
178     // Should not set Referer after a redirect from a secure resource to non-secure one.
179     if (m_shouldClearReferrerOnHTTPSToHTTPRedirect && !request.url().protocolIs("https") && WebCore::protocolIs(request.httpReferrer(), "https"))
180         request.clearHTTPReferrer();
181     
182     const auto& url = request.url();
183     m_user = url.user();
184     m_password = url.pass();
185     m_lastHTTPMethod = request.httpMethod();
186     request.removeCredentials();
187     
188     if (!protocolHostAndPortAreEqual(request.url(), redirectResponse.url())) {
189         // The network layer might carry over some headers from the original request that
190         // we want to strip here because the redirect is cross-origin.
191         request.clearHTTPAuthorization();
192         request.clearHTTPOrigin();
193 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
194     } else {
195         // Only consider applying authentication credentials if this is actually a redirect and the redirect
196         // URL didn't include credentials of its own.
197         if (m_user.isEmpty() && m_password.isEmpty() && !redirectResponse.isNull()) {
198             auto credential = m_session->networkStorageSession().credentialStorage().get(m_partition, request.url());
199             if (!credential.isEmpty()) {
200                 m_initialCredential = credential;
201
202                 // FIXME: Support Digest authentication, and Proxy-Authorization.
203                 applyBasicAuthorizationHeader(request, m_initialCredential);
204             }
205         }
206 #endif
207     }
208     
209     if (m_client)
210         m_client->willPerformHTTPRedirection(WTFMove(redirectResponse), WTFMove(request), WTFMove(completionHandler));
211     else {
212         ASSERT_NOT_REACHED();
213         completionHandler({ });
214     }
215 }
216
217 void NetworkDataTaskCocoa::setPendingDownloadLocation(const WTF::String& filename, const SandboxExtension::Handle& sandboxExtensionHandle, bool allowOverwrite)
218 {
219     NetworkDataTask::setPendingDownloadLocation(filename, sandboxExtensionHandle, allowOverwrite);
220
221     ASSERT(!m_sandboxExtension);
222     m_sandboxExtension = SandboxExtension::create(sandboxExtensionHandle);
223     if (m_sandboxExtension)
224         m_sandboxExtension->consume();
225
226     m_task.get()._pathToDownloadTaskFile = m_pendingDownloadLocation;
227
228     if (allowOverwrite && WebCore::fileExists(m_pendingDownloadLocation))
229         WebCore::deleteFile(filename);
230 }
231
232 bool NetworkDataTaskCocoa::tryPasswordBasedAuthentication(const WebCore::AuthenticationChallenge& challenge, const ChallengeCompletionHandler& completionHandler)
233 {
234     if (!challenge.protectionSpace().isPasswordBased())
235         return false;
236     
237     if (!m_user.isNull() && !m_password.isNull()) {
238         auto persistence = m_storedCredentials == WebCore::StoredCredentials::AllowStoredCredentials ? WebCore::CredentialPersistenceForSession : WebCore::CredentialPersistenceNone;
239         completionHandler(AuthenticationChallengeDisposition::UseCredential, WebCore::Credential(m_user, m_password, persistence));
240         m_user = String();
241         m_password = String();
242         return true;
243     }
244
245 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
246     if (m_storedCredentials == WebCore::AllowStoredCredentials) {
247         if (!m_initialCredential.isEmpty() || challenge.previousFailureCount()) {
248             // The stored credential wasn't accepted, stop using it.
249             // There is a race condition here, since a different credential might have already been stored by another ResourceHandle,
250             // but the observable effect should be very minor, if any.
251             m_session->networkStorageSession().credentialStorage().remove(m_partition, challenge.protectionSpace());
252         }
253
254         if (!challenge.previousFailureCount()) {
255             auto credential = m_session->networkStorageSession().credentialStorage().get(m_partition, challenge.protectionSpace());
256             if (!credential.isEmpty() && credential != m_initialCredential) {
257                 ASSERT(credential.persistence() == WebCore::CredentialPersistenceNone);
258                 if (challenge.failureResponse().httpStatusCode() == 401) {
259                     // Store the credential back, possibly adding it as a default for this directory.
260                     m_session->networkStorageSession().credentialStorage().set(m_partition, credential, challenge.protectionSpace(), challenge.failureResponse().url());
261                 }
262                 completionHandler(AuthenticationChallengeDisposition::UseCredential, credential);
263                 return true;
264             }
265         }
266     }
267 #endif
268
269     if (!challenge.proposedCredential().isEmpty() && !challenge.previousFailureCount()) {
270         completionHandler(AuthenticationChallengeDisposition::UseCredential, challenge.proposedCredential());
271         return true;
272     }
273     
274     return false;
275 }
276
277 void NetworkDataTaskCocoa::transferSandboxExtensionToDownload(Download& download)
278 {
279     download.setSandboxExtension(WTFMove(m_sandboxExtension));
280 }
281
282 #if !USE(CFURLCONNECTION)
283 static bool certificatesMatch(SecTrustRef trust1, SecTrustRef trust2)
284 {
285     if (!trust1 || !trust2)
286         return false;
287
288     CFIndex count1 = SecTrustGetCertificateCount(trust1);
289     CFIndex count2 = SecTrustGetCertificateCount(trust2);
290     if (count1 != count2)
291         return false;
292
293     for (CFIndex i = 0; i < count1; i++) {
294         auto cert1 = SecTrustGetCertificateAtIndex(trust1, i);
295         auto cert2 = SecTrustGetCertificateAtIndex(trust2, i);
296         RELEASE_ASSERT(cert1);
297         RELEASE_ASSERT(cert2);
298         if (!CFEqual(cert1, cert2))
299             return false;
300     }
301
302     return true;
303 }
304 #endif
305
306 bool NetworkDataTaskCocoa::allowsSpecificHTTPSCertificateForHost(const WebCore::AuthenticationChallenge& challenge)
307 {
308     const String& host = challenge.protectionSpace().host();
309     NSArray *certificates = [NSURLRequest allowsSpecificHTTPSCertificateForHost:host];
310     if (!certificates)
311         return false;
312     
313     bool requireServerCertificates = challenge.protectionSpace().authenticationScheme() == WebCore::ProtectionSpaceAuthenticationScheme::ProtectionSpaceAuthenticationSchemeServerTrustEvaluationRequested;
314     RetainPtr<SecPolicyRef> policy = adoptCF(SecPolicyCreateSSL(requireServerCertificates, host.createCFString().get()));
315     
316     SecTrustRef trustRef = nullptr;
317     if (SecTrustCreateWithCertificates((CFArrayRef)certificates, policy.get(), &trustRef) != noErr)
318         return false;
319     RetainPtr<SecTrustRef> trust = adoptCF(trustRef);
320
321 #if USE(CFURLCONNECTION)
322     notImplemented();
323     return false;
324 #else
325     return certificatesMatch(trust.get(), challenge.nsURLAuthenticationChallenge().protectionSpace.serverTrust);
326 #endif
327 }
328
329 String NetworkDataTaskCocoa::suggestedFilename() const
330 {
331     if (!m_suggestedFilename.isEmpty())
332         return m_suggestedFilename;
333     return m_task.get().response.suggestedFilename;
334 }
335
336 void NetworkDataTaskCocoa::cancel()
337 {
338     [m_task cancel];
339 }
340
341 void NetworkDataTaskCocoa::resume()
342 {
343     if (m_scheduledFailureType != NoFailure)
344         m_failureTimer.startOneShot(0);
345     [m_task resume];
346 }
347
348 void NetworkDataTaskCocoa::suspend()
349 {
350     if (m_failureTimer.isActive())
351         m_failureTimer.stop();
352     [m_task suspend];
353 }
354
355 NetworkDataTask::State NetworkDataTaskCocoa::state() const
356 {
357     switch ([m_task state]) {
358     case NSURLSessionTaskStateRunning:
359         return State::Running;
360     case NSURLSessionTaskStateSuspended:
361         return State::Suspended;
362     case NSURLSessionTaskStateCanceling:
363         return State::Canceling;
364     case NSURLSessionTaskStateCompleted:
365         return State::Completed;
366     }
367
368     ASSERT_NOT_REACHED();
369     return State::Completed;
370 }
371
372 WebCore::Credential serverTrustCredential(const WebCore::AuthenticationChallenge& challenge)
373 {
374 #if USE(CFURLCONNECTION)
375     notImplemented();
376     return { };
377 #else
378     return WebCore::Credential([NSURLCredential credentialForTrust:challenge.nsURLAuthenticationChallenge().protectionSpace.serverTrust]);
379 #endif
380 }
381
382 }
383
384 #endif