| /* |
| * Copyright (C) 2016-2018 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #import "config.h" |
| #import "NetworkDataTaskCocoa.h" |
| |
| #import "AuthenticationChallengeDisposition.h" |
| #import "AuthenticationManager.h" |
| #import "DeviceManagementSPI.h" |
| #import "Download.h" |
| #import "DownloadProxyMessages.h" |
| #import "Logging.h" |
| #import "NetworkProcess.h" |
| #import "NetworkSessionCocoa.h" |
| #import "WebCoreArgumentCoders.h" |
| #import <WebCore/AuthenticationChallenge.h> |
| #import <WebCore/NetworkStorageSession.h> |
| #import <WebCore/NotImplemented.h> |
| #import <WebCore/RegistrableDomain.h> |
| #import <WebCore/ResourceRequest.h> |
| #import <WebCore/TimingAllowOrigin.h> |
| #import <pal/spi/cf/CFNetworkSPI.h> |
| #import <wtf/BlockPtr.h> |
| #import <wtf/FileSystem.h> |
| #import <wtf/MainThread.h> |
| #import <wtf/ProcessPrivilege.h> |
| #import <wtf/SystemTracing.h> |
| #import <wtf/WeakObjCPtr.h> |
| #import <wtf/cocoa/RuntimeApplicationChecksCocoa.h> |
| #import <wtf/text/Base64.h> |
| |
| #if HAVE(NW_ACTIVITY) |
| #import <pal/spi/cocoa/NSURLConnectionSPI.h> |
| #endif |
| |
| #if USE(APPLE_INTERNAL_SDK) |
| #import <WebKitAdditions/NetworkDataTaskCocoaAdditions.h> |
| #else |
| static void processPCMRequest(WebCore::PrivateClickMeasurement::PcmDataCarried, NSMutableURLRequest *) { } |
| #endif |
| |
| namespace WebKit { |
| |
| void setPCMDataCarriedOnRequest(WebCore::PrivateClickMeasurement::PcmDataCarried pcmDataCarried, NSMutableURLRequest *request) |
| { |
| processPCMRequest(pcmDataCarried, request); |
| } |
| |
| #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION) |
| static void applyBasicAuthorizationHeader(WebCore::ResourceRequest& request, const WebCore::Credential& credential) |
| { |
| request.setHTTPHeaderField(WebCore::HTTPHeaderName::Authorization, credential.serializationForBasicAuthorizationHeader()); |
| } |
| #endif |
| |
| static float toNSURLSessionTaskPriority(WebCore::ResourceLoadPriority priority) |
| { |
| switch (priority) { |
| case WebCore::ResourceLoadPriority::VeryLow: |
| return 0; |
| case WebCore::ResourceLoadPriority::Low: |
| return 0.25; |
| case WebCore::ResourceLoadPriority::Medium: |
| return 0.5; |
| case WebCore::ResourceLoadPriority::High: |
| return 0.75; |
| case WebCore::ResourceLoadPriority::VeryHigh: |
| return 1; |
| } |
| |
| ASSERT_NOT_REACHED(); |
| return NSURLSessionTaskPriorityDefault; |
| } |
| |
| void NetworkDataTaskCocoa::applySniffingPoliciesAndBindRequestToInferfaceIfNeeded(RetainPtr<NSURLRequest>& nsRequest, bool shouldContentSniff, bool shouldContentEncodingSniff) |
| { |
| #if !USE(CFNETWORK_CONTENT_ENCODING_SNIFFING_OVERRIDE) |
| UNUSED_PARAM(shouldContentEncodingSniff); |
| #endif |
| |
| auto& cocoaSession = static_cast<NetworkSessionCocoa&>(*m_session); |
| auto& boundInterfaceIdentifier = cocoaSession.boundInterfaceIdentifier(); |
| if (shouldContentSniff |
| #if USE(CFNETWORK_CONTENT_ENCODING_SNIFFING_OVERRIDE) |
| && shouldContentEncodingSniff |
| #endif |
| && boundInterfaceIdentifier.isNull()) |
| return; |
| |
| auto mutableRequest = adoptNS([nsRequest mutableCopy]); |
| |
| #if USE(CFNETWORK_CONTENT_ENCODING_SNIFFING_OVERRIDE) |
| if (!shouldContentEncodingSniff) |
| [mutableRequest _setProperty:@YES forKey:(NSString *)kCFURLRequestContentDecoderSkipURLCheck]; |
| #endif |
| |
| if (!shouldContentSniff) |
| [mutableRequest _setProperty:@NO forKey:(NSString *)_kCFURLConnectionPropertyShouldSniff]; |
| |
| if (!boundInterfaceIdentifier.isNull()) |
| [mutableRequest setBoundInterfaceIdentifier:boundInterfaceIdentifier]; |
| |
| nsRequest = mutableRequest.autorelease(); |
| } |
| |
| #if ENABLE(INTELLIGENT_TRACKING_PREVENTION) |
| NSHTTPCookieStorage *NetworkDataTaskCocoa::statelessCookieStorage() |
| { |
| static NeverDestroyed<RetainPtr<NSHTTPCookieStorage>> statelessCookieStorage; |
| if (!statelessCookieStorage.get()) { |
| statelessCookieStorage.get() = adoptNS([[NSHTTPCookieStorage alloc] _initWithIdentifier:nil private:YES]); |
| statelessCookieStorage.get().get().cookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever; |
| } |
| ASSERT(statelessCookieStorage.get().get().cookies.count == 0); |
| return statelessCookieStorage.get().get(); |
| } |
| |
| #if HAVE(CFNETWORK_CNAME_AND_COOKIE_TRANSFORM_SPI) |
| // FIXME: Remove these selector checks when macOS Big Sur has shipped. |
| // https://bugs.webkit.org/show_bug.cgi?id=215280 |
| static bool hasCNAMEAndCookieTransformSPI(NSURLSessionDataTask* task) |
| { |
| return [task respondsToSelector:@selector(_cookieTransformCallback)] |
| && [task respondsToSelector:@selector(_resolvedCNAMEChain)]; |
| } |
| |
| static WebCore::RegistrableDomain lastCNAMEDomain(NSArray<NSString *> *cnames) |
| { |
| if (auto* lastResolvedCNAMEInChain = [cnames lastObject]) { |
| auto cname = String(lastResolvedCNAMEInChain); |
| if (cname.endsWith('.')) |
| cname.remove(cname.length() - 1); |
| return WebCore::RegistrableDomain::uncheckedCreateFromHost(cname); |
| } |
| |
| return { }; |
| } |
| |
| void NetworkDataTaskCocoa::updateFirstPartyInfoForSession(const URL& requestURL) |
| { |
| if (!hasCNAMEAndCookieTransformSPI(m_task.get()) || !networkSession() || !networkSession()->networkStorageSession() || !networkSession()->networkStorageSession()->resourceLoadStatisticsEnabled() || requestURL.host().isEmpty()) |
| return; |
| |
| auto cnameDomain = lastCNAMEDomain([m_task _resolvedCNAMEChain]); |
| if (!cnameDomain.isEmpty()) |
| networkSession()->setFirstPartyHostCNAMEDomain(requestURL.host().toString(), WTFMove(cnameDomain)); |
| } |
| |
| void NetworkDataTaskCocoa::applyCookiePolicyForThirdPartyCNAMECloaking(const WebCore::ResourceRequest& request) |
| { |
| if (!hasCNAMEAndCookieTransformSPI(m_task.get()) || isTopLevelNavigation() || !networkSession() || !networkSession()->networkStorageSession() || !networkSession()->networkStorageSession()->resourceLoadStatisticsEnabled()) |
| return; |
| |
| if (isThirdPartyRequest(request)) { |
| m_task.get()._cookieTransformCallback = nil; |
| return; |
| } |
| |
| // Cap expiry of incoming cookies in response if it is a same-site |
| // subresource but it resolves to a different CNAME than the top |
| // site request, a.k.a. third-party CNAME cloaking. |
| auto firstPartyURL = request.firstPartyForCookies(); |
| auto firstPartyHostCNAME = networkSession()->firstPartyHostCNAMEDomain(firstPartyURL.host().toString()); |
| |
| m_task.get()._cookieTransformCallback = makeBlockPtr([requestURL = crossThreadCopy(request.url()), firstPartyURL = crossThreadCopy(firstPartyURL), firstPartyHostCNAME = crossThreadCopy(firstPartyHostCNAME), thirdPartyCNAMEDomainForTesting = crossThreadCopy(networkSession()->thirdPartyCNAMEDomainForTesting()), ageCapForCNAMECloakedCookies = crossThreadCopy(m_ageCapForCNAMECloakedCookies), weakTask = WeakObjCPtr<NSURLSessionDataTask>(m_task.get()), debugLoggingEnabled = networkSession()->networkStorageSession()->resourceLoadStatisticsDebugLoggingEnabled()] (NSArray<NSHTTPCookie*> *cookiesSetInResponse) -> NSArray<NSHTTPCookie*> * { |
| auto task = weakTask.get(); |
| if (!task || ![cookiesSetInResponse count]) |
| return cookiesSetInResponse; |
| |
| auto cnameDomain = lastCNAMEDomain([task _resolvedCNAMEChain]); |
| if (cnameDomain.isEmpty()) { |
| if (!thirdPartyCNAMEDomainForTesting) |
| return cookiesSetInResponse; |
| cnameDomain = *thirdPartyCNAMEDomainForTesting; |
| } |
| |
| // CNAME cloaking is a first-party sub resource that resolves |
| // through a CNAME that differs from the first-party domain and |
| // also differs from the top frame host's CNAME, if one exists. |
| if (!cnameDomain.matches(firstPartyURL) && (!firstPartyHostCNAME || cnameDomain != *firstPartyHostCNAME)) { |
| |
| NSUInteger count = [cookiesSetInResponse count]; |
| // Don't use RetainPtr here. This array has to be retained and |
| // auto released to not be released before returned to the code |
| // executing the block. |
| auto* cappedCookies = [NSMutableArray arrayWithCapacity:count]; |
| for (NSUInteger i = 0; i < count; ++i) { |
| NSHTTPCookie *cookie = (NSHTTPCookie *)[cookiesSetInResponse objectAtIndex:i]; |
| cookie = WebCore::NetworkStorageSession::capExpiryOfPersistentCookie(cookie, ageCapForCNAMECloakedCookies); |
| [cappedCookies addObject:cookie]; |
| if (debugLoggingEnabled) |
| RELEASE_LOG_INFO(ITPDebug, "Capped the expiry of third-party CNAME cloaked cookie named %{public}s.", [[cookie name] UTF8String]); |
| } |
| return cappedCookies; |
| } |
| |
| return cookiesSetInResponse; |
| }).get(); |
| } |
| #endif |
| |
| void NetworkDataTaskCocoa::blockCookies() |
| { |
| ASSERT(hasProcessPrivilege(ProcessPrivilege::CanAccessRawCookies)); |
| |
| if (m_hasBeenSetToUseStatelessCookieStorage) |
| return; |
| |
| [m_task _setExplicitCookieStorage:statelessCookieStorage()._cookieStorage]; |
| m_hasBeenSetToUseStatelessCookieStorage = true; |
| } |
| |
| void NetworkDataTaskCocoa::unblockCookies() |
| { |
| ASSERT(hasProcessPrivilege(ProcessPrivilege::CanAccessRawCookies)); |
| |
| if (!m_hasBeenSetToUseStatelessCookieStorage) |
| return; |
| |
| if (auto* storageSession = m_session->networkStorageSession()) { |
| [m_task _setExplicitCookieStorage:storageSession->nsCookieStorage()._cookieStorage]; |
| m_hasBeenSetToUseStatelessCookieStorage = false; |
| } |
| } |
| |
| // FIXME: Temporary fix for <rdar://problem/60089022> until content can be updated. |
| bool NetworkDataTaskCocoa::needsFirstPartyCookieBlockingLatchModeQuirk(const URL& firstPartyURL, const URL& requestURL, const URL& redirectingURL) const |
| { |
| using RegistrableDomain = WebCore::RegistrableDomain; |
| static NeverDestroyed<HashMap<RegistrableDomain, RegistrableDomain>> quirkPairs = [] { |
| HashMap<RegistrableDomain, RegistrableDomain> map; |
| map.add(RegistrableDomain::uncheckedCreateFromRegistrableDomainString("ymail.com"_s), RegistrableDomain::uncheckedCreateFromRegistrableDomainString("yahoo.com"_s)); |
| map.add(RegistrableDomain::uncheckedCreateFromRegistrableDomainString("aolmail.com"_s), RegistrableDomain::uncheckedCreateFromRegistrableDomainString("aol.com"_s)); |
| return map; |
| }(); |
| |
| RegistrableDomain firstPartyDomain { firstPartyURL }; |
| RegistrableDomain requestDomain { requestURL }; |
| if (firstPartyDomain != requestDomain) |
| return false; |
| |
| RegistrableDomain redirectingDomain { redirectingURL }; |
| auto quirk = quirkPairs.get().find(redirectingDomain); |
| if (quirk == quirkPairs.get().end()) |
| return false; |
| |
| return quirk->value == requestDomain; |
| } |
| #endif |
| |
| static void updateTaskWithFirstPartyForSameSiteCookies(NSURLSessionDataTask* task, const WebCore::ResourceRequest& request) |
| { |
| if (request.isSameSiteUnspecified()) |
| return; |
| #if HAVE(FOUNDATION_WITH_SAME_SITE_COOKIE_SUPPORT) |
| static NSURL *emptyURL = [[NSURL alloc] initWithString:@""]; |
| task._siteForCookies = request.isSameSite() ? task.currentRequest.URL : emptyURL; |
| task._isTopLevelNavigation = request.isTopSite(); |
| #else |
| UNUSED_PARAM(task); |
| #endif |
| } |
| |
| static inline bool computeIsAlwaysOnLoggingAllowed(NetworkSession& session) |
| { |
| if (session.networkProcess().sessionIsControlledByAutomation(session.sessionID())) |
| return true; |
| |
| return session.sessionID().isAlwaysOnLoggingAllowed(); |
| } |
| |
| NetworkDataTaskCocoa::NetworkDataTaskCocoa(NetworkSession& session, NetworkDataTaskClient& client, const NetworkLoadParameters& parameters) |
| : NetworkDataTask(session, client, parameters.request, parameters.storedCredentialsPolicy, parameters.shouldClearReferrerOnHTTPSToHTTPRedirect, parameters.isMainFrameNavigation) |
| , m_sessionWrapper(static_cast<NetworkSessionCocoa&>(session).sessionWrapperForTask(parameters.webPageProxyID, parameters.request, parameters.storedCredentialsPolicy, parameters.isNavigatingToAppBoundDomain)) |
| , m_frameID(parameters.webFrameID) |
| , m_pageID(parameters.webPageID) |
| , m_webPageProxyID(parameters.webPageProxyID) |
| , m_isForMainResourceNavigationForAnyFrame(parameters.isMainResourceNavigationForAnyFrame) |
| , m_isAlwaysOnLoggingAllowed(computeIsAlwaysOnLoggingAllowed(session)) |
| , m_shouldRelaxThirdPartyCookieBlocking(parameters.shouldRelaxThirdPartyCookieBlocking) |
| , m_sourceOrigin(parameters.sourceOrigin) |
| { |
| auto request = parameters.request; |
| auto url = request.url(); |
| if (m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use && url.protocolIsInHTTPFamily()) { |
| m_user = url.user(); |
| m_password = url.password(); |
| request.removeCredentials(); |
| url = request.url(); |
| |
| #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION) |
| if (auto* storageSession = m_session->networkStorageSession()) { |
| if (m_user.isEmpty() && m_password.isEmpty()) |
| m_initialCredential = storageSession->credentialStorage().get(m_partition, url); |
| else |
| storageSession->credentialStorage().set(m_partition, WebCore::Credential(m_user, m_password, WebCore::CredentialPersistenceNone), url); |
| } |
| #endif |
| } |
| |
| #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION) |
| if (!m_initialCredential.isEmpty()) { |
| // FIXME: Support Digest authentication, and Proxy-Authorization. |
| applyBasicAuthorizationHeader(request, m_initialCredential); |
| } |
| #endif |
| |
| bool shouldBlockCookies = false; |
| #if ENABLE(INTELLIGENT_TRACKING_PREVENTION) |
| shouldBlockCookies = m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::EphemeralStateless; |
| if (auto* networkStorageSession = session.networkStorageSession()) { |
| if (!shouldBlockCookies) |
| shouldBlockCookies = networkStorageSession->shouldBlockCookies(request, frameID(), pageID(), m_shouldRelaxThirdPartyCookieBlocking); |
| } |
| #endif |
| restrictRequestReferrerToOriginIfNeeded(request); |
| |
| RetainPtr<NSURLRequest> nsRequest = request.nsURLRequest(WebCore::HTTPBodyUpdatePolicy::UpdateHTTPBody); |
| RetainPtr<NSMutableURLRequest> mutableRequest = adoptNS([nsRequest.get() mutableCopy]); |
| |
| #if ENABLE(APP_PRIVACY_REPORT) |
| mutableRequest.get().attribution = request.isAppInitiated() ? NSURLRequestAttributionDeveloper : NSURLRequestAttributionUser; |
| #endif |
| |
| nsRequest = mutableRequest; |
| |
| #if ENABLE(APP_PRIVACY_REPORT) |
| m_session->appPrivacyReportTestingData().didLoadAppInitiatedRequest(nsRequest.get().attribution == NSURLRequestAttributionDeveloper); |
| #endif |
| |
| applySniffingPoliciesAndBindRequestToInferfaceIfNeeded(nsRequest, parameters.contentSniffingPolicy == WebCore::ContentSniffingPolicy::SniffContent && !url.isLocalFile(), parameters.contentEncodingSniffingPolicy == WebCore::ContentEncodingSniffingPolicy::Sniff); |
| |
| m_task = [m_sessionWrapper->session dataTaskWithRequest:nsRequest.get()]; |
| |
| WTFBeginSignpost(m_task.get(), "DataTask", "%{public}s pri: %.2f preconnect: %d", url.string().ascii().data(), toNSURLSessionTaskPriority(request.priority()), parameters.shouldPreconnectOnly == PreconnectOnly::Yes); |
| |
| RELEASE_ASSERT(!m_sessionWrapper->dataTaskMap.contains([m_task taskIdentifier])); |
| m_sessionWrapper->dataTaskMap.add([m_task taskIdentifier], this); |
| LOG(NetworkSession, "%lu Creating NetworkDataTask with URL %s", (unsigned long)[m_task taskIdentifier], [nsRequest URL].absoluteString.UTF8String); |
| |
| if (parameters.shouldPreconnectOnly == PreconnectOnly::Yes) { |
| #if ENABLE(SERVER_PRECONNECT) |
| m_task.get()._preconnect = true; |
| #else |
| ASSERT_NOT_REACHED(); |
| #endif |
| } |
| |
| #if ENABLE(INTELLIGENT_TRACKING_PREVENTION) |
| #if HAVE(CFNETWORK_CNAME_AND_COOKIE_TRANSFORM_SPI) |
| applyCookiePolicyForThirdPartyCNAMECloaking(request); |
| #endif |
| if (shouldBlockCookies) { |
| #if !RELEASE_LOG_DISABLED |
| if (m_session->shouldLogCookieInformation()) |
| RELEASE_LOG_IF(isAlwaysOnLoggingAllowed(), Network, "%p - NetworkDataTaskCocoa::logCookieInformation: pageID=%" PRIu64 ", frameID=%" PRIu64 ", taskID=%lu: Blocking cookies for URL %s", this, pageID().toUInt64(), frameID().toUInt64(), (unsigned long)[m_task taskIdentifier], [nsRequest URL].absoluteString.UTF8String); |
| #else |
| LOG(NetworkSession, "%lu Blocking cookies for URL %s", (unsigned long)[m_task taskIdentifier], [nsRequest URL].absoluteString.UTF8String); |
| #endif |
| blockCookies(); |
| } |
| #endif |
| |
| if (WebCore::ResourceRequest::resourcePrioritiesEnabled()) |
| m_task.get().priority = toNSURLSessionTaskPriority(request.priority()); |
| |
| updateTaskWithFirstPartyForSameSiteCookies(m_task.get(), request); |
| |
| #if HAVE(NW_ACTIVITY) |
| if (parameters.networkActivityTracker) |
| m_task.get()._nw_activity = parameters.networkActivityTracker->getPlatformObject(); |
| #endif |
| } |
| |
| NetworkDataTaskCocoa::~NetworkDataTaskCocoa() |
| { |
| if (!m_task || !m_sessionWrapper) |
| return; |
| |
| RELEASE_ASSERT(m_sessionWrapper->dataTaskMap.get([m_task taskIdentifier]) == this); |
| m_sessionWrapper->dataTaskMap.remove([m_task taskIdentifier]); |
| } |
| |
| void NetworkDataTaskCocoa::didSendData(uint64_t totalBytesSent, uint64_t totalBytesExpectedToSend) |
| { |
| WTFEmitSignpost(m_task.get(), "DataTask", "sent %llu bytes (expected %llu bytes)", totalBytesSent, totalBytesExpectedToSend); |
| |
| if (m_client) |
| m_client->didSendData(totalBytesSent, totalBytesExpectedToSend); |
| } |
| |
| void NetworkDataTaskCocoa::didReceiveChallenge(WebCore::AuthenticationChallenge&& challenge, NegotiatedLegacyTLS negotiatedLegacyTLS, ChallengeCompletionHandler&& completionHandler) |
| { |
| WTFEmitSignpost(m_task.get(), "DataTask", "received challenge"); |
| |
| if (tryPasswordBasedAuthentication(challenge, completionHandler)) |
| return; |
| |
| if (m_client) |
| m_client->didReceiveChallenge(WTFMove(challenge), negotiatedLegacyTLS, WTFMove(completionHandler)); |
| else { |
| ASSERT_NOT_REACHED(); |
| completionHandler(AuthenticationChallengeDisposition::PerformDefaultHandling, { }); |
| } |
| } |
| |
| void NetworkDataTaskCocoa::didNegotiateModernTLS(const URL& url) |
| { |
| if (m_client) |
| m_client->didNegotiateModernTLS(url); |
| } |
| |
| void NetworkDataTaskCocoa::didCompleteWithError(const WebCore::ResourceError& error, const WebCore::NetworkLoadMetrics& networkLoadMetrics) |
| { |
| if (error.isNull()) |
| WTFEndSignpost(m_task.get(), "DataTask", "completed"); |
| else |
| WTFEndSignpost(m_task.get(), "DataTask", "failed"); |
| |
| if (m_client) |
| m_client->didCompleteWithError(error, networkLoadMetrics); |
| } |
| |
| void NetworkDataTaskCocoa::didReceiveData(const WebCore::SharedBuffer& data) |
| { |
| WTFEmitSignpost(m_task.get(), "DataTask", "received %zd bytes", data.size()); |
| |
| if (m_client) |
| m_client->didReceiveData(data); |
| } |
| |
| void NetworkDataTaskCocoa::didReceiveResponse(WebCore::ResourceResponse&& response, NegotiatedLegacyTLS negotiatedLegacyTLS, WebKit::ResponseCompletionHandler&& completionHandler) |
| { |
| WTFEmitSignpost(m_task.get(), "DataTask", "received response headers"); |
| #if HAVE(CFNETWORK_CNAME_AND_COOKIE_TRANSFORM_SPI) |
| if (isTopLevelNavigation()) |
| updateFirstPartyInfoForSession(response.url()); |
| #endif |
| NetworkDataTask::didReceiveResponse(WTFMove(response), negotiatedLegacyTLS, WTFMove(completionHandler)); |
| } |
| |
| void NetworkDataTaskCocoa::willPerformHTTPRedirection(WebCore::ResourceResponse&& redirectResponse, WebCore::ResourceRequest&& request, RedirectCompletionHandler&& completionHandler) |
| { |
| WTFEmitSignpost(m_task.get(), "DataTask", "redirect"); |
| |
| networkLoadMetrics().hasCrossOriginRedirect = networkLoadMetrics().hasCrossOriginRedirect || !WebCore::SecurityOrigin::create(request.url())->canRequest(redirectResponse.url()); |
| |
| if (redirectResponse.httpStatusCode() == 307 || redirectResponse.httpStatusCode() == 308) { |
| ASSERT(m_lastHTTPMethod == request.httpMethod()); |
| WebCore::FormData* body = m_firstRequest.httpBody(); |
| if (body && !body->isEmpty() && !equalLettersIgnoringASCIICase(m_lastHTTPMethod, "get")) |
| request.setHTTPBody(body); |
| |
| String originalContentType = m_firstRequest.httpContentType(); |
| if (!originalContentType.isEmpty()) |
| request.setHTTPHeaderField(WebCore::HTTPHeaderName::ContentType, originalContentType); |
| } else if (redirectResponse.httpStatusCode() == 303 && equalLettersIgnoringASCIICase(m_firstRequest.httpMethod(), "head")) // FIXME: (rdar://problem/13706454). |
| request.setHTTPMethod("HEAD"_s); |
| |
| // Should not set Referer after a redirect from a secure resource to non-secure one. |
| if (m_shouldClearReferrerOnHTTPSToHTTPRedirect && !request.url().protocolIs("https") && WTF::protocolIs(request.httpReferrer(), "https")) |
| request.clearHTTPReferrer(); |
| |
| const auto& url = request.url(); |
| m_user = url.user(); |
| m_password = url.password(); |
| m_lastHTTPMethod = request.httpMethod(); |
| request.removeCredentials(); |
| |
| if (!protocolHostAndPortAreEqual(request.url(), redirectResponse.url())) { |
| // The network layer might carry over some headers from the original request that |
| // we want to strip here because the redirect is cross-origin. |
| request.clearHTTPAuthorization(); |
| request.clearHTTPOrigin(); |
| |
| } else { |
| if (auto authorization = m_firstRequest.httpHeaderField(WebCore::HTTPHeaderName::Authorization); !authorization.isNull() && linkedOnOrAfter(SDKVersion::FirstWithAuthorizationHeaderOnSameOriginRedirects)) |
| request.setHTTPHeaderField(WebCore::HTTPHeaderName::Authorization, authorization); |
| |
| #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION) |
| // Only consider applying authentication credentials if this is actually a redirect and the redirect |
| // URL didn't include credentials of its own. |
| if (m_user.isEmpty() && m_password.isEmpty() && !redirectResponse.isNull()) { |
| auto credential = m_session->networkStorageSession() ? m_session->networkStorageSession()->credentialStorage().get(m_partition, request.url()) : WebCore::Credential(); |
| if (!credential.isEmpty()) { |
| m_initialCredential = credential; |
| |
| // FIXME: Support Digest authentication, and Proxy-Authorization. |
| applyBasicAuthorizationHeader(request, m_initialCredential); |
| } |
| #endif |
| } |
| } |
| |
| if (isTopLevelNavigation()) |
| request.setFirstPartyForCookies(request.url()); |
| |
| #if ENABLE(APP_PRIVACY_REPORT) |
| request.setIsAppInitiated(request.nsURLRequest(WebCore::HTTPBodyUpdatePolicy::DoNotUpdateHTTPBody).attribution == NSURLRequestAttributionDeveloper); |
| #endif |
| |
| #if ENABLE(INTELLIGENT_TRACKING_PREVENTION) |
| #if HAVE(CFNETWORK_CNAME_AND_COOKIE_TRANSFORM_SPI) |
| applyCookiePolicyForThirdPartyCNAMECloaking(request); |
| #endif |
| if (!m_hasBeenSetToUseStatelessCookieStorage) { |
| if (m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::EphemeralStateless |
| || (m_session->networkStorageSession() && m_session->networkStorageSession()->shouldBlockCookies(request, m_frameID, m_pageID, m_shouldRelaxThirdPartyCookieBlocking))) |
| blockCookies(); |
| } else if (m_storedCredentialsPolicy != WebCore::StoredCredentialsPolicy::EphemeralStateless && needsFirstPartyCookieBlockingLatchModeQuirk(request.firstPartyForCookies(), request.url(), redirectResponse.url())) |
| unblockCookies(); |
| #if !RELEASE_LOG_DISABLED |
| if (m_session->shouldLogCookieInformation()) |
| RELEASE_LOG_IF(isAlwaysOnLoggingAllowed(), Network, "%p - NetworkDataTaskCocoa::willPerformHTTPRedirection::logCookieInformation: pageID=%" PRIu64 ", frameID=%" PRIu64 ", taskID=%lu: %s cookies for redirect URL %s", this, m_pageID.toUInt64(), m_frameID.toUInt64(), (unsigned long)[m_task taskIdentifier], (m_hasBeenSetToUseStatelessCookieStorage ? "Blocking" : "Not blocking"), request.url().string().utf8().data()); |
| #else |
| LOG(NetworkSession, "%lu %s cookies for redirect URL %s", (unsigned long)[m_task taskIdentifier], (m_hasBeenSetToUseStatelessCookieStorage ? "Blocking" : "Not blocking"), request.url().string().utf8().data()); |
| #endif |
| #endif |
| |
| updateTaskWithFirstPartyForSameSiteCookies(m_task.get(), request); |
| |
| if (m_client) |
| m_client->willPerformHTTPRedirection(WTFMove(redirectResponse), WTFMove(request), [completionHandler = WTFMove(completionHandler), this, weakThis = WeakPtr { *this }] (auto&& request) mutable { |
| if (!weakThis || !m_session) |
| return completionHandler({ }); |
| if (!request.isNull()) |
| restrictRequestReferrerToOriginIfNeeded(request); |
| completionHandler(WTFMove(request)); |
| }); |
| else { |
| ASSERT_NOT_REACHED(); |
| completionHandler({ }); |
| } |
| } |
| |
| void NetworkDataTaskCocoa::setPendingDownloadLocation(const WTF::String& filename, SandboxExtension::Handle&& sandboxExtensionHandle, bool allowOverwrite) |
| { |
| NetworkDataTask::setPendingDownloadLocation(filename, { }, allowOverwrite); |
| |
| ASSERT(!m_sandboxExtension); |
| m_sandboxExtension = SandboxExtension::create(WTFMove(sandboxExtensionHandle)); |
| if (m_sandboxExtension) |
| m_sandboxExtension->consume(); |
| |
| m_task.get()._pathToDownloadTaskFile = m_pendingDownloadLocation; |
| |
| if (allowOverwrite && FileSystem::fileExists(m_pendingDownloadLocation)) |
| FileSystem::deleteFile(filename); |
| } |
| |
| bool NetworkDataTaskCocoa::tryPasswordBasedAuthentication(const WebCore::AuthenticationChallenge& challenge, ChallengeCompletionHandler& completionHandler) |
| { |
| if (!challenge.protectionSpace().isPasswordBased()) |
| return false; |
| |
| if (!m_user.isNull() && !m_password.isNull()) { |
| auto persistence = m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use ? WebCore::CredentialPersistenceForSession : WebCore::CredentialPersistenceNone; |
| completionHandler(AuthenticationChallengeDisposition::UseCredential, WebCore::Credential(m_user, m_password, persistence)); |
| m_user = String(); |
| m_password = String(); |
| return true; |
| } |
| |
| #if USE(CREDENTIAL_STORAGE_WITH_NETWORK_SESSION) |
| if (m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use) { |
| if (!m_initialCredential.isEmpty() || challenge.previousFailureCount()) { |
| // The stored credential wasn't accepted, stop using it. |
| // There is a race condition here, since a different credential might have already been stored by another ResourceHandle, |
| // but the observable effect should be very minor, if any. |
| if (auto* storageSession = m_session->networkStorageSession()) |
| storageSession->credentialStorage().remove(m_partition, challenge.protectionSpace()); |
| } |
| |
| if (!challenge.previousFailureCount()) { |
| auto credential = m_session->networkStorageSession() ? m_session->networkStorageSession()->credentialStorage().get(m_partition, challenge.protectionSpace()) : WebCore::Credential(); |
| if (!credential.isEmpty() && credential != m_initialCredential) { |
| ASSERT(credential.persistence() == WebCore::CredentialPersistenceNone); |
| if (challenge.failureResponse().httpStatusCode() == 401) { |
| // Store the credential back, possibly adding it as a default for this directory. |
| if (auto* storageSession = m_session->networkStorageSession()) |
| storageSession->credentialStorage().set(m_partition, credential, challenge.protectionSpace(), challenge.failureResponse().url()); |
| } |
| completionHandler(AuthenticationChallengeDisposition::UseCredential, credential); |
| return true; |
| } |
| } |
| } |
| #endif |
| |
| if (!challenge.proposedCredential().isEmpty() && !challenge.previousFailureCount()) { |
| completionHandler(AuthenticationChallengeDisposition::UseCredential, challenge.proposedCredential()); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void NetworkDataTaskCocoa::transferSandboxExtensionToDownload(Download& download) |
| { |
| download.setSandboxExtension(WTFMove(m_sandboxExtension)); |
| } |
| |
| String NetworkDataTaskCocoa::suggestedFilename() const |
| { |
| if (!m_suggestedFilename.isEmpty()) |
| return m_suggestedFilename; |
| return m_task.get().response.suggestedFilename; |
| } |
| |
| void NetworkDataTaskCocoa::cancel() |
| { |
| WTFEmitSignpost(m_task.get(), "DataTask", "cancel"); |
| [m_task cancel]; |
| } |
| |
| void NetworkDataTaskCocoa::resume() |
| { |
| WTFEmitSignpost(m_task.get(), "DataTask", "resume"); |
| |
| if (m_failureScheduled) |
| return; |
| |
| auto& cocoaSession = static_cast<NetworkSessionCocoa&>(*m_session); |
| if (cocoaSession.deviceManagementRestrictionsEnabled() && m_isForMainResourceNavigationForAnyFrame) { |
| auto didDetermineDeviceRestrictionPolicyForURL = makeBlockPtr([this, protectedThis = Ref { *this }](BOOL isBlocked) mutable { |
| callOnMainRunLoop([this, protectedThis = WTFMove(protectedThis), isBlocked] { |
| if (isBlocked) { |
| scheduleFailure(FailureType::RestrictedURL); |
| return; |
| } |
| |
| [m_task resume]; |
| }); |
| }); |
| |
| #if HAVE(DEVICE_MANAGEMENT) |
| if (cocoaSession.allLoadsBlockedByDeviceManagementRestrictionsForTesting()) |
| didDetermineDeviceRestrictionPolicyForURL(true); |
| else { |
| RetainPtr<NSURL> urlToCheck = [m_task currentRequest].URL; |
| [cocoaSession.deviceManagementPolicyMonitor() requestPoliciesForWebsites:@[ urlToCheck.get() ] completionHandler:makeBlockPtr([didDetermineDeviceRestrictionPolicyForURL, urlToCheck] (NSDictionary<NSURL *, NSNumber *> *policies, NSError *error) { |
| bool isBlocked = error || policies[urlToCheck.get()].integerValue != DMFPolicyOK; |
| didDetermineDeviceRestrictionPolicyForURL(isBlocked); |
| }).get()]; |
| } |
| #else |
| didDetermineDeviceRestrictionPolicyForURL(cocoaSession.allLoadsBlockedByDeviceManagementRestrictionsForTesting()); |
| #endif |
| return; |
| } |
| |
| [m_task resume]; |
| } |
| |
| NetworkDataTask::State NetworkDataTaskCocoa::state() const |
| { |
| switch ([m_task state]) { |
| case NSURLSessionTaskStateRunning: |
| return State::Running; |
| case NSURLSessionTaskStateSuspended: |
| return State::Suspended; |
| case NSURLSessionTaskStateCanceling: |
| return State::Canceling; |
| case NSURLSessionTaskStateCompleted: |
| return State::Completed; |
| } |
| |
| ASSERT_NOT_REACHED(); |
| return State::Completed; |
| } |
| |
| WebCore::Credential serverTrustCredential(const WebCore::AuthenticationChallenge& challenge) |
| { |
| return WebCore::Credential([NSURLCredential credentialForTrust:challenge.nsURLAuthenticationChallenge().protectionSpace.serverTrust]); |
| } |
| |
| bool NetworkDataTaskCocoa::isAlwaysOnLoggingAllowed() const |
| { |
| return m_isAlwaysOnLoggingAllowed; |
| } |
| |
| String NetworkDataTaskCocoa::description() const |
| { |
| return String([m_task description]); |
| } |
| |
| void NetworkDataTaskCocoa::setH2PingCallback(const URL& url, CompletionHandler<void(Expected<WTF::Seconds, WebCore::ResourceError>&&)>&& completionHandler) |
| { |
| #if HAVE(PRECONNECT_PING) |
| ASSERT(m_task.get()._preconnect); |
| auto handler = CompletionHandlerWithFinalizer<void(Expected<WTF::Seconds, WebCore::ResourceError>&&)>(WTFMove(completionHandler), [url] (Function<void(Expected<WTF::Seconds, WebCore::ResourceError>&&)>& completionHandler) { |
| completionHandler(makeUnexpected(WebCore::internalError(url))); |
| }); |
| [m_task getUnderlyingHTTPConnectionInfoWithCompletionHandler:makeBlockPtr([completionHandler = WTFMove(handler), url] (_NSHTTPConnectionInfo *connectionInfo) mutable { |
| if (!connectionInfo.isValid) |
| return completionHandler(makeUnexpected(WebCore::internalError(url))); |
| [connectionInfo sendPingWithReceiveHandler:makeBlockPtr([completionHandler = WTFMove(completionHandler)](NSError *error, NSTimeInterval interval) mutable { |
| completionHandler(Seconds(interval)); |
| }).get()]; |
| }).get()]; |
| #else |
| ASSERT_NOT_REACHED(); |
| return completionHandler(makeUnexpected(WebCore::internalError(url))); |
| #endif |
| } |
| |
| void NetworkDataTaskCocoa::setPriority(WebCore::ResourceLoadPriority priority) |
| { |
| if (!WebCore::ResourceRequest::resourcePrioritiesEnabled()) |
| return; |
| m_task.get().priority = toNSURLSessionTaskPriority(priority); |
| } |
| |
| void NetworkDataTaskCocoa::checkTAO(const WebCore::ResourceResponse& response) |
| { |
| if (networkLoadMetrics().failsTAOCheck) |
| return; |
| |
| RefPtr<WebCore::SecurityOrigin> origin; |
| if (isTopLevelNavigation()) |
| origin = WebCore::SecurityOrigin::create(firstRequest().url()); |
| else |
| origin = m_sourceOrigin; |
| |
| if (origin) |
| networkLoadMetrics().failsTAOCheck = !passesTimingAllowOriginCheck(response, *origin); |
| } |
| |
| } |