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