2 * Copyright (C) 2016-2017 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
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.
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.
27 #import "NetworkDataTaskCocoa.h"
29 #if USE(NETWORK_SESSION)
31 #import "AuthenticationManager.h"
33 #import "DownloadProxyMessages.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>
50 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
51 static void applyBasicAuthorizationHeader(WebCore::ResourceRequest& request, const WebCore::Credential& credential)
53 String authenticationHeader = "Basic " + base64Encode(String(credential.user() + ":" + credential.password()).utf8());
54 request.setHTTPHeaderField(WebCore::HTTPHeaderName::Authorization, authenticationHeader);
58 static float toNSURLSessionTaskPriority(WebCore::ResourceLoadPriority priority)
61 case WebCore::ResourceLoadPriority::VeryLow:
63 case WebCore::ResourceLoadPriority::Low:
65 case WebCore::ResourceLoadPriority::Medium:
67 case WebCore::ResourceLoadPriority::High:
69 case WebCore::ResourceLoadPriority::VeryHigh:
74 return NSURLSessionTaskPriorityDefault;
77 void NetworkDataTaskCocoa::applySniffingPoliciesAndBindRequestToInferfaceIfNeeded(NSURLRequest*& nsRequest, bool shouldContentSniff, bool shouldContentEncodingSniff)
80 UNUSED_PARAM(shouldContentEncodingSniff);
81 #elif __MAC_OS_X_VERSION_MIN_REQUIRED < 101302
82 shouldContentEncodingSniff = true;
84 auto& cocoaSession = static_cast<NetworkSessionCocoa&>(m_session.get());
85 if (shouldContentSniff && shouldContentEncodingSniff && cocoaSession.m_boundInterfaceIdentifier.isNull())
88 auto mutableRequest = adoptNS([nsRequest mutableCopy]);
90 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101302
91 if (!shouldContentEncodingSniff)
92 [mutableRequest _setProperty:@(YES) forKey:(NSString *)kCFURLRequestContentDecoderSkipURLCheck];
95 if (!shouldContentSniff)
96 [mutableRequest _setProperty:@(NO) forKey:(NSString *)_kCFURLConnectionPropertyShouldSniff];
98 if (!cocoaSession.m_boundInterfaceIdentifier.isNull())
99 [mutableRequest setBoundInterfaceIdentifier:cocoaSession.m_boundInterfaceIdentifier];
101 nsRequest = mutableRequest.autorelease();
104 NetworkDataTaskCocoa::NetworkDataTaskCocoa(NetworkSession& session, NetworkDataTaskClient& client, const WebCore::ResourceRequest& requestWithCredentials, uint64_t frameID, uint64_t pageID, WebCore::StoredCredentialsPolicy storedCredentialsPolicy, WebCore::ContentSniffingPolicy shouldContentSniff, WebCore::ContentEncodingSniffingPolicy shouldContentEncodingSniff, bool shouldClearReferrerOnHTTPSToHTTPRedirect, PreconnectOnly shouldPreconnectOnly)
105 : NetworkDataTask(session, client, requestWithCredentials, storedCredentialsPolicy, shouldClearReferrerOnHTTPSToHTTPRedirect)
109 if (m_scheduledFailureType != NoFailure)
112 auto request = requestWithCredentials;
113 auto url = request.url();
114 if (storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use && url.protocolIsInHTTPFamily()) {
116 m_password = url.pass();
117 request.removeCredentials();
120 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
121 if (m_user.isEmpty() && m_password.isEmpty())
122 m_initialCredential = m_session->networkStorageSession().credentialStorage().get(m_partition, url);
124 m_session->networkStorageSession().credentialStorage().set(m_partition, WebCore::Credential(m_user, m_password, WebCore::CredentialPersistenceNone), url);
128 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
129 if (!m_initialCredential.isEmpty()) {
130 // FIXME: Support Digest authentication, and Proxy-Authorization.
131 applyBasicAuthorizationHeader(request, m_initialCredential);
135 NSURLRequest *nsRequest = request.nsURLRequest(WebCore::UpdateHTTPBody);
136 applySniffingPoliciesAndBindRequestToInferfaceIfNeeded(nsRequest, shouldContentSniff == WebCore::SniffContent && !url.isLocalFile(), shouldContentEncodingSniff == WebCore::ContentEncodingSniffingPolicy::Sniff);
138 if (session.networkStorageSession().shouldBlockCookies(request)) {
139 storedCredentialsPolicy = WebCore::StoredCredentialsPolicy::DoNotUse;
140 m_storedCredentialsPolicy = WebCore::StoredCredentialsPolicy::DoNotUse;
143 auto& cocoaSession = static_cast<NetworkSessionCocoa&>(m_session.get());
144 if (storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use) {
145 m_task = [cocoaSession.m_sessionWithCredentialStorage dataTaskWithRequest:nsRequest];
146 ASSERT(!cocoaSession.m_dataTaskMapWithCredentials.contains([m_task taskIdentifier]));
147 cocoaSession.m_dataTaskMapWithCredentials.add([m_task taskIdentifier], this);
148 LOG(NetworkSession, "%llu Creating stateless NetworkDataTask with URL %s", [m_task taskIdentifier], nsRequest.URL.absoluteString.UTF8String);
150 m_task = [cocoaSession.m_statelessSession dataTaskWithRequest:nsRequest];
151 ASSERT(!cocoaSession.m_dataTaskMapWithoutState.contains([m_task taskIdentifier]));
152 cocoaSession.m_dataTaskMapWithoutState.add([m_task taskIdentifier], this);
153 LOG(NetworkSession, "%llu Creating NetworkDataTask with URL %s", [m_task taskIdentifier], nsRequest.URL.absoluteString.UTF8String);
156 if (shouldPreconnectOnly == PreconnectOnly::Yes) {
157 #if ENABLE(SERVER_PRECONNECT)
158 m_task.get()._preconnect = true;
160 ASSERT_NOT_REACHED();
164 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
165 if (storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use) {
166 String storagePartition = session.networkStorageSession().cookieStoragePartition(request, m_frameID, m_pageID);
167 if (!storagePartition.isEmpty()) {
168 LOG(NetworkSession, "%llu Partitioning cookies for URL %s", [m_task taskIdentifier], nsRequest.URL.absoluteString.UTF8String);
169 m_task.get()._storagePartitionIdentifier = storagePartition;
174 if (WebCore::ResourceRequest::resourcePrioritiesEnabled())
175 m_task.get().priority = toNSURLSessionTaskPriority(request.priority());
178 NetworkDataTaskCocoa::~NetworkDataTaskCocoa()
183 auto& cocoaSession = static_cast<NetworkSessionCocoa&>(m_session.get());
184 if (m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use) {
185 ASSERT(cocoaSession.m_dataTaskMapWithCredentials.get([m_task taskIdentifier]) == this);
186 cocoaSession.m_dataTaskMapWithCredentials.remove([m_task taskIdentifier]);
188 ASSERT(cocoaSession.m_dataTaskMapWithoutState.get([m_task taskIdentifier]) == this);
189 cocoaSession.m_dataTaskMapWithoutState.remove([m_task taskIdentifier]);
193 void NetworkDataTaskCocoa::didSendData(uint64_t totalBytesSent, uint64_t totalBytesExpectedToSend)
196 m_client->didSendData(totalBytesSent, totalBytesExpectedToSend);
199 void NetworkDataTaskCocoa::didReceiveChallenge(const WebCore::AuthenticationChallenge& challenge, ChallengeCompletionHandler&& completionHandler)
201 if (tryPasswordBasedAuthentication(challenge, completionHandler))
205 m_client->didReceiveChallenge(challenge, WTFMove(completionHandler));
207 ASSERT_NOT_REACHED();
208 completionHandler(AuthenticationChallengeDisposition::PerformDefaultHandling, { });
212 void NetworkDataTaskCocoa::didCompleteWithError(const WebCore::ResourceError& error, const WebCore::NetworkLoadMetrics& networkLoadMetrics)
215 m_client->didCompleteWithError(error, networkLoadMetrics);
218 void NetworkDataTaskCocoa::didReceiveData(Ref<WebCore::SharedBuffer>&& data)
221 m_client->didReceiveData(WTFMove(data));
224 static bool shouldChangePartition(const String& requiredStoragePartition, const String& currentStoragePartition)
226 // The need for a partion change is according to the following:
227 // currentStoragePartition: null "" abc
228 // requiredStoragePartition: "" false false true
229 // abc true true false
230 // xyz true true true
231 return !((requiredStoragePartition.isEmpty() && currentStoragePartition.isEmpty()) || currentStoragePartition == requiredStoragePartition);
234 void NetworkDataTaskCocoa::willPerformHTTPRedirection(WebCore::ResourceResponse&& redirectResponse, WebCore::ResourceRequest&& request, RedirectCompletionHandler&& completionHandler)
236 if (redirectResponse.httpStatusCode() == 307 || redirectResponse.httpStatusCode() == 308) {
237 ASSERT(m_lastHTTPMethod == request.httpMethod());
238 WebCore::FormData* body = m_firstRequest.httpBody();
239 if (body && !body->isEmpty() && !equalLettersIgnoringASCIICase(m_lastHTTPMethod, "get"))
240 request.setHTTPBody(body);
242 String originalContentType = m_firstRequest.httpContentType();
243 if (!originalContentType.isEmpty())
244 request.setHTTPHeaderField(WebCore::HTTPHeaderName::ContentType, originalContentType);
247 // Should not set Referer after a redirect from a secure resource to non-secure one.
248 if (m_shouldClearReferrerOnHTTPSToHTTPRedirect && !request.url().protocolIs("https") && WebCore::protocolIs(request.httpReferrer(), "https"))
249 request.clearHTTPReferrer();
251 const auto& url = request.url();
253 m_password = url.pass();
254 m_lastHTTPMethod = request.httpMethod();
255 request.removeCredentials();
257 if (!protocolHostAndPortAreEqual(request.url(), redirectResponse.url())) {
258 // The network layer might carry over some headers from the original request that
259 // we want to strip here because the redirect is cross-origin.
260 request.clearHTTPAuthorization();
261 request.clearHTTPOrigin();
262 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
264 // Only consider applying authentication credentials if this is actually a redirect and the redirect
265 // URL didn't include credentials of its own.
266 if (m_user.isEmpty() && m_password.isEmpty() && !redirectResponse.isNull()) {
267 auto credential = m_session->networkStorageSession().credentialStorage().get(m_partition, request.url());
268 if (!credential.isEmpty()) {
269 m_initialCredential = credential;
271 // FIXME: Support Digest authentication, and Proxy-Authorization.
272 applyBasicAuthorizationHeader(request, m_initialCredential);
278 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
279 if (m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use) {
280 String requiredStoragePartition = m_session->networkStorageSession().cookieStoragePartition(request, m_frameID, m_pageID);
281 if (shouldChangePartition(requiredStoragePartition, m_task.get()._storagePartitionIdentifier)) {
282 LOG(NetworkSession, "%llu %s cookies for redirected URL %s", [m_task taskIdentifier], (requiredStoragePartition.isEmpty() ? "Not partitioning" : "Partitioning"), request.url().string().utf8().data());
283 m_task.get()._storagePartitionIdentifier = requiredStoragePartition;
289 m_client->willPerformHTTPRedirection(WTFMove(redirectResponse), WTFMove(request), WTFMove(completionHandler));
291 ASSERT_NOT_REACHED();
292 completionHandler({ });
296 void NetworkDataTaskCocoa::setPendingDownloadLocation(const WTF::String& filename, SandboxExtension::Handle&& sandboxExtensionHandle, bool allowOverwrite)
298 NetworkDataTask::setPendingDownloadLocation(filename, { }, allowOverwrite);
300 ASSERT(!m_sandboxExtension);
301 m_sandboxExtension = SandboxExtension::create(WTFMove(sandboxExtensionHandle));
302 if (m_sandboxExtension)
303 m_sandboxExtension->consume();
305 m_task.get()._pathToDownloadTaskFile = m_pendingDownloadLocation;
307 if (allowOverwrite && WebCore::FileSystem::fileExists(m_pendingDownloadLocation))
308 WebCore::FileSystem::deleteFile(filename);
311 bool NetworkDataTaskCocoa::tryPasswordBasedAuthentication(const WebCore::AuthenticationChallenge& challenge, ChallengeCompletionHandler& completionHandler)
313 if (!challenge.protectionSpace().isPasswordBased())
316 if (!m_user.isNull() && !m_password.isNull()) {
317 auto persistence = m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use ? WebCore::CredentialPersistenceForSession : WebCore::CredentialPersistenceNone;
318 completionHandler(AuthenticationChallengeDisposition::UseCredential, WebCore::Credential(m_user, m_password, persistence));
320 m_password = String();
324 #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION)
325 if (m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use) {
326 if (!m_initialCredential.isEmpty() || challenge.previousFailureCount()) {
327 // The stored credential wasn't accepted, stop using it.
328 // There is a race condition here, since a different credential might have already been stored by another ResourceHandle,
329 // but the observable effect should be very minor, if any.
330 m_session->networkStorageSession().credentialStorage().remove(m_partition, challenge.protectionSpace());
333 if (!challenge.previousFailureCount()) {
334 auto credential = m_session->networkStorageSession().credentialStorage().get(m_partition, challenge.protectionSpace());
335 if (!credential.isEmpty() && credential != m_initialCredential) {
336 ASSERT(credential.persistence() == WebCore::CredentialPersistenceNone);
337 if (challenge.failureResponse().httpStatusCode() == 401) {
338 // Store the credential back, possibly adding it as a default for this directory.
339 m_session->networkStorageSession().credentialStorage().set(m_partition, credential, challenge.protectionSpace(), challenge.failureResponse().url());
341 completionHandler(AuthenticationChallengeDisposition::UseCredential, credential);
348 if (!challenge.proposedCredential().isEmpty() && !challenge.previousFailureCount()) {
349 completionHandler(AuthenticationChallengeDisposition::UseCredential, challenge.proposedCredential());
356 void NetworkDataTaskCocoa::transferSandboxExtensionToDownload(Download& download)
358 download.setSandboxExtension(WTFMove(m_sandboxExtension));
361 String NetworkDataTaskCocoa::suggestedFilename() const
363 if (!m_suggestedFilename.isEmpty())
364 return m_suggestedFilename;
365 return m_task.get().response.suggestedFilename;
368 void NetworkDataTaskCocoa::cancel()
373 void NetworkDataTaskCocoa::resume()
375 if (m_scheduledFailureType != NoFailure)
376 m_failureTimer.startOneShot(0_s);
380 void NetworkDataTaskCocoa::suspend()
382 if (m_failureTimer.isActive())
383 m_failureTimer.stop();
387 NetworkDataTask::State NetworkDataTaskCocoa::state() const
389 switch ([m_task state]) {
390 case NSURLSessionTaskStateRunning:
391 return State::Running;
392 case NSURLSessionTaskStateSuspended:
393 return State::Suspended;
394 case NSURLSessionTaskStateCanceling:
395 return State::Canceling;
396 case NSURLSessionTaskStateCompleted:
397 return State::Completed;
400 ASSERT_NOT_REACHED();
401 return State::Completed;
404 WebCore::Credential serverTrustCredential(const WebCore::AuthenticationChallenge& challenge)
406 return WebCore::Credential([NSURLCredential credentialForTrust:challenge.nsURLAuthenticationChallenge().protectionSpace.serverTrust]);