blob: 86dbfb1e0c3488d9569e4de6dcd156a40375f77d [file] [log] [blame]
/*
* Copyright (C) 2018 Sony Interactive Entertainment Inc.
*
* 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.
*/
#include "config.h"
#include "NetworkDataTaskCurl.h"
#include "AuthenticationChallengeDisposition.h"
#include "AuthenticationManager.h"
#include "NetworkSessionCurl.h"
#include <WebCore/AuthenticationChallenge.h>
#include <WebCore/CookieJar.h>
#include <WebCore/CurlRequest.h>
#include <WebCore/NetworkLoadMetrics.h>
#include <WebCore/NetworkStorageSession.h>
#include <WebCore/NotImplemented.h>
#include <WebCore/ResourceError.h>
#include <WebCore/SameSiteInfo.h>
#include <WebCore/SynchronousLoaderClient.h>
namespace WebKit {
using namespace WebCore;
NetworkDataTaskCurl::NetworkDataTaskCurl(NetworkSession& session, NetworkDataTaskClient& client, const ResourceRequest& requestWithCredentials, StoredCredentialsPolicy storedCredentialsPolicy, ContentSniffingPolicy shouldContentSniff, ContentEncodingSniffingPolicy, bool shouldClearReferrerOnHTTPSToHTTPRedirect, bool dataTaskIsForMainFrameNavigation)
: NetworkDataTask(session, client, requestWithCredentials, storedCredentialsPolicy, shouldClearReferrerOnHTTPSToHTTPRedirect, dataTaskIsForMainFrameNavigation)
{
if (m_scheduledFailureType != NoFailure)
return;
m_startTime = MonotonicTime::now();
auto request = requestWithCredentials;
if (request.url().protocolIsInHTTPFamily()) {
if (m_storedCredentialsPolicy == StoredCredentialsPolicy::Use) {
auto url = request.url();
m_user = url.user();
m_password = url.pass();
request.removeCredentials();
if (m_user.isEmpty() && m_password.isEmpty())
m_initialCredential = m_session->networkStorageSession()->credentialStorage().get(m_partition, request.url());
else
m_session->networkStorageSession()->credentialStorage().set(m_partition, Credential(m_user, m_password, CredentialPersistenceNone), request.url());
}
}
m_curlRequest = createCurlRequest(WTFMove(request));
if (!m_initialCredential.isEmpty()) {
m_curlRequest->setUserPass(m_initialCredential.user(), m_initialCredential.password());
m_curlRequest->setAuthenticationScheme(ProtectionSpaceAuthenticationSchemeHTTPBasic);
}
m_curlRequest->setStartTime(m_startTime);
m_curlRequest->start();
}
NetworkDataTaskCurl::~NetworkDataTaskCurl()
{
invalidateAndCancel();
}
void NetworkDataTaskCurl::resume()
{
ASSERT(m_state != State::Running);
if (m_state == State::Canceling || m_state == State::Completed)
return;
m_state = State::Running;
if (m_scheduledFailureType != NoFailure) {
ASSERT(m_failureTimer.isActive());
return;
}
if (m_curlRequest)
m_curlRequest->resume();
}
void NetworkDataTaskCurl::cancel()
{
if (m_state == State::Canceling || m_state == State::Completed)
return;
m_state = State::Canceling;
if (m_curlRequest)
m_curlRequest->cancel();
}
void NetworkDataTaskCurl::invalidateAndCancel()
{
cancel();
if (m_curlRequest)
m_curlRequest->invalidateClient();
}
NetworkDataTask::State NetworkDataTaskCurl::state() const
{
return m_state;
}
Ref<CurlRequest> NetworkDataTaskCurl::createCurlRequest(ResourceRequest&& request, RequestStatus status)
{
if (status == RequestStatus::NewRequest)
appendCookieHeader(request);
// Creates a CurlRequest in suspended state.
// Then, NetworkDataTaskCurl::resume() will be called and communication resumes.
const auto captureMetrics = shouldCaptureExtraNetworkLoadMetrics() ? CurlRequest::CaptureNetworkLoadMetrics::Extended : CurlRequest::CaptureNetworkLoadMetrics::Basic;
return CurlRequest::create(request, *this, CurlRequest::ShouldSuspend::Yes, CurlRequest::EnableMultipart::No, captureMetrics);
}
void NetworkDataTaskCurl::curlDidSendData(CurlRequest&, unsigned long long totalBytesSent, unsigned long long totalBytesExpectedToSend)
{
auto protectedThis = makeRef(*this);
if (state() == State::Canceling || state() == State::Completed || !m_client)
return;
m_client->didSendData(totalBytesSent, totalBytesExpectedToSend);
}
void NetworkDataTaskCurl::curlDidReceiveResponse(CurlRequest& request, CurlResponse&& receivedResponse)
{
auto protectedThis = makeRef(*this);
if (state() == State::Canceling || state() == State::Completed || !m_client)
return;
m_response = ResourceResponse(receivedResponse);
m_response.setCertificateInfo(WTFMove(receivedResponse.certificateInfo));
m_response.setDeprecatedNetworkLoadMetrics(WTFMove(receivedResponse.networkLoadMetrics));
handleCookieHeaders(request.resourceRequest(), receivedResponse);
if (m_response.shouldRedirect()) {
willPerformHTTPRedirection();
return;
}
if (m_response.isUnauthorized() && receivedResponse.availableHttpAuth) {
tryHttpAuthentication(AuthenticationChallenge(receivedResponse, m_authFailureCount, m_response));
m_authFailureCount++;
return;
}
if (m_response.isProxyAuthenticationRequired() && receivedResponse.availableProxyAuth) {
tryProxyAuthentication(AuthenticationChallenge(receivedResponse, 0, m_response));
return;
}
invokeDidReceiveResponse();
}
void NetworkDataTaskCurl::curlDidReceiveBuffer(CurlRequest&, Ref<SharedBuffer>&& buffer)
{
auto protectedThis = makeRef(*this);
if (state() == State::Canceling || state() == State::Completed || (!m_client && !isDownload()))
return;
m_client->didReceiveData(WTFMove(buffer));
}
void NetworkDataTaskCurl::curlDidComplete(CurlRequest&, NetworkLoadMetrics&& networkLoadMetrics)
{
if (state() == State::Canceling || state() == State::Completed || (!m_client && !isDownload()))
return;
m_client->didCompleteWithError({ }, WTFMove(networkLoadMetrics));
}
void NetworkDataTaskCurl::curlDidFailWithError(CurlRequest& request, ResourceError&& resourceError, CertificateInfo&& certificateInfo)
{
if (state() == State::Canceling || state() == State::Completed || (!m_client && !isDownload()))
return;
if (resourceError.isSSLCertVerificationError()) {
tryServerTrustEvaluation(AuthenticationChallenge(request.resourceRequest().url(), certificateInfo, resourceError));
return;
}
m_client->didCompleteWithError(resourceError);
}
bool NetworkDataTaskCurl::shouldRedirectAsGET(const ResourceRequest& request, bool crossOrigin)
{
if (request.httpMethod() == "GET" || request.httpMethod() == "HEAD")
return false;
if (!request.url().protocolIsInHTTPFamily())
return true;
if (m_response.isSeeOther())
return true;
if ((m_response.isMovedPermanently() || m_response.isFound()) && (request.httpMethod() == "POST"))
return true;
if (crossOrigin && (request.httpMethod() == "DELETE"))
return true;
return false;
}
void NetworkDataTaskCurl::invokeDidReceiveResponse()
{
didReceiveResponse(ResourceResponse(m_response), [this, protectedThis = makeRef(*this)](PolicyAction policyAction) {
if (m_state == State::Canceling || m_state == State::Completed)
return;
switch (policyAction) {
case PolicyAction::Use:
if (m_curlRequest)
m_curlRequest->completeDidReceiveResponse();
break;
case PolicyAction::Ignore:
break;
default:
notImplemented();
break;
}
});
}
void NetworkDataTaskCurl::willPerformHTTPRedirection()
{
static const int maxRedirects = 20;
if (m_redirectCount++ > maxRedirects) {
m_client->didCompleteWithError(ResourceError::httpError(CURLE_TOO_MANY_REDIRECTS, m_response.url()));
return;
}
ResourceRequest request = m_firstRequest;
URL redirectedURL = URL(m_response.url(), m_response.httpHeaderField(HTTPHeaderName::Location));
if (!redirectedURL.hasFragmentIdentifier() && request.url().hasFragmentIdentifier())
redirectedURL.setFragmentIdentifier(request.url().fragmentIdentifier());
request.setURL(redirectedURL);
// Should not set Referer after a redirect from a secure resource to non-secure one.
if (m_shouldClearReferrerOnHTTPSToHTTPRedirect && !request.url().protocolIs("https") && protocolIs(request.httpReferrer(), "https"))
request.clearHTTPReferrer();
bool isCrossOrigin = !protocolHostAndPortAreEqual(m_firstRequest.url(), request.url());
if (!equalLettersIgnoringASCIICase(request.httpMethod(), "get")) {
// Change request method to GET if change was made during a previous redirection or if current redirection says so.
if (!request.url().protocolIsInHTTPFamily() || shouldRedirectAsGET(request, isCrossOrigin)) {
request.setHTTPMethod("GET");
request.setHTTPBody(nullptr);
request.clearHTTPContentType();
}
}
bool didChangeCredential = false;
const auto& url = request.url();
m_user = url.user();
m_password = url.pass();
m_lastHTTPMethod = request.httpMethod();
request.removeCredentials();
if (isCrossOrigin) {
// 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 (m_storedCredentialsPolicy == StoredCredentialsPolicy::Use) {
// 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()) {
auto credential = m_session->networkStorageSession()->credentialStorage().get(m_partition, request.url());
if (!credential.isEmpty()) {
m_initialCredential = credential;
didChangeCredential = true;
}
}
}
auto response = ResourceResponse(m_response);
m_client->willPerformHTTPRedirection(WTFMove(response), WTFMove(request), [this, protectedThis = makeRef(*this), didChangeCredential, isCrossOrigin](const ResourceRequest& newRequest) {
if (newRequest.isNull() || m_state == State::Canceling)
return;
if (m_curlRequest)
m_curlRequest->cancel();
if (newRequest.url().protocolIsInHTTPFamily() && isCrossOrigin)
m_startTime = MonotonicTime::now();
auto requestCopy = newRequest;
m_curlRequest = createCurlRequest(WTFMove(requestCopy));
if (didChangeCredential && !m_initialCredential.isEmpty()) {
m_curlRequest->setUserPass(m_initialCredential.user(), m_initialCredential.password());
m_curlRequest->setAuthenticationScheme(ProtectionSpaceAuthenticationSchemeHTTPBasic);
}
m_curlRequest->setStartTime(m_startTime);
m_curlRequest->start();
if (m_state != State::Suspended) {
m_state = State::Suspended;
resume();
}
});
}
void NetworkDataTaskCurl::tryHttpAuthentication(AuthenticationChallenge&& challenge)
{
if (!m_user.isNull() && !m_password.isNull()) {
auto persistence = m_storedCredentialsPolicy == WebCore::StoredCredentialsPolicy::Use ? WebCore::CredentialPersistenceForSession : WebCore::CredentialPersistenceNone;
restartWithCredential(challenge.protectionSpace(), Credential(m_user, m_password, persistence));
m_user = String();
m_password = String();
return;
}
if (m_storedCredentialsPolicy == 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
// NetworkDataTask, but the observable effect should be very minor, if any.
m_session->networkStorageSession()->credentialStorage().remove(m_partition, challenge.protectionSpace());
}
if (!challenge.previousFailureCount()) {
auto credential = m_session->networkStorageSession()->credentialStorage().get(m_partition, challenge.protectionSpace());
if (!credential.isEmpty() && credential != m_initialCredential) {
ASSERT(credential.persistence() == CredentialPersistenceNone);
if (challenge.failureResponse().isUnauthorized()) {
// Store the credential back, possibly adding it as a default for this directory.
m_session->networkStorageSession()->credentialStorage().set(m_partition, credential, challenge.protectionSpace(), challenge.failureResponse().url());
}
restartWithCredential(challenge.protectionSpace(), credential);
return;
}
}
}
m_client->didReceiveChallenge(AuthenticationChallenge(challenge), [this, protectedThis = makeRef(*this), challenge](AuthenticationChallengeDisposition disposition, const Credential& credential) {
if (m_state == State::Canceling || m_state == State::Completed)
return;
if (disposition == AuthenticationChallengeDisposition::Cancel) {
cancel();
m_client->didCompleteWithError(ResourceError::httpError(CURLE_COULDNT_RESOLVE_HOST, m_response.url()));
return;
}
if (disposition == AuthenticationChallengeDisposition::UseCredential && !credential.isEmpty()) {
if (m_storedCredentialsPolicy == StoredCredentialsPolicy::Use) {
if (credential.persistence() == CredentialPersistenceForSession || credential.persistence() == CredentialPersistencePermanent)
m_session->networkStorageSession()->credentialStorage().set(m_partition, credential, challenge.protectionSpace(), challenge.failureResponse().url());
}
restartWithCredential(challenge.protectionSpace(), credential);
return;
}
invokeDidReceiveResponse();
});
}
void NetworkDataTaskCurl::tryProxyAuthentication(WebCore::AuthenticationChallenge&& challenge)
{
m_client->didReceiveChallenge(AuthenticationChallenge(challenge), [this, protectedThis = makeRef(*this), challenge](AuthenticationChallengeDisposition disposition, const Credential& credential) {
if (m_state == State::Canceling || m_state == State::Completed)
return;
if (disposition == AuthenticationChallengeDisposition::Cancel) {
cancel();
m_client->didCompleteWithError(ResourceError::httpError(CURLE_COULDNT_RESOLVE_PROXY, m_response.url()));
return;
}
if (disposition == AuthenticationChallengeDisposition::UseCredential && !credential.isEmpty()) {
CurlContext::singleton().setProxyUserPass(credential.user(), credential.password());
CurlContext::singleton().setDefaultProxyAuthMethod();
auto requestCredential = m_curlRequest ? Credential(m_curlRequest->user(), m_curlRequest->password(), CredentialPersistenceNone) : Credential();
restartWithCredential(challenge.protectionSpace(), requestCredential);
return;
}
invokeDidReceiveResponse();
});
}
void NetworkDataTaskCurl::tryServerTrustEvaluation(AuthenticationChallenge&& challenge)
{
m_client->didReceiveChallenge(AuthenticationChallenge(challenge), [this, protectedThis = makeRef(*this), challenge](AuthenticationChallengeDisposition disposition, const Credential& credential) {
if (m_state == State::Canceling || m_state == State::Completed)
return;
if (disposition == AuthenticationChallengeDisposition::UseCredential && !credential.isEmpty()) {
auto requestCredential = m_curlRequest ? Credential(m_curlRequest->user(), m_curlRequest->password(), CredentialPersistenceNone) : Credential();
restartWithCredential(challenge.protectionSpace(), requestCredential);
return;
}
cancel();
m_client->didCompleteWithError(challenge.error());
});
}
void NetworkDataTaskCurl::restartWithCredential(const ProtectionSpace& protectionSpace, const Credential& credential)
{
ASSERT(m_curlRequest);
auto previousRequest = m_curlRequest->resourceRequest();
auto shouldDisableServerTrustEvaluation = protectionSpace.authenticationScheme() == ProtectionSpaceAuthenticationSchemeServerTrustEvaluationRequested || m_curlRequest->isServerTrustEvaluationDisabled();
m_curlRequest->cancel();
m_curlRequest = createCurlRequest(WTFMove(previousRequest), RequestStatus::ReusedRequest);
m_curlRequest->setAuthenticationScheme(protectionSpace.authenticationScheme());
m_curlRequest->setUserPass(credential.user(), credential.password());
if (shouldDisableServerTrustEvaluation)
m_curlRequest->disableServerTrustEvaluation();
m_curlRequest->setStartTime(m_startTime);
m_curlRequest->start();
if (m_state != State::Suspended) {
m_state = State::Suspended;
resume();
}
}
void NetworkDataTaskCurl::appendCookieHeader(WebCore::ResourceRequest& request)
{
const auto* storageSession = m_session->networkStorageSession();
const auto& cookieJar = storageSession->cookieStorage();
auto includeSecureCookies = request.url().protocolIs("https") ? IncludeSecureCookies::Yes : IncludeSecureCookies::No;
auto cookieHeaderField = cookieJar.cookieRequestHeaderFieldValue(*storageSession, request.firstPartyForCookies(), WebCore::SameSiteInfo::create(request), request.url(), WTF::nullopt, WTF::nullopt, includeSecureCookies).first;
if (!cookieHeaderField.isEmpty())
request.addHTTPHeaderField(HTTPHeaderName::Cookie, cookieHeaderField);
}
void NetworkDataTaskCurl::handleCookieHeaders(const WebCore::ResourceRequest& request, const CurlResponse& response)
{
static const auto setCookieHeader = "set-cookie: ";
const auto* storageSession = m_session->networkStorageSession();
const auto& cookieJar = storageSession->cookieStorage();
for (auto header : response.headers) {
if (header.startsWithIgnoringASCIICase(setCookieHeader)) {
String setCookieString = header.right(header.length() - strlen(setCookieHeader));
cookieJar.setCookiesFromHTTPResponse(*storageSession, request.firstPartyForCookies(), response.url, setCookieString);
}
}
}
} // namespace WebKit