| /* |
| * Copyright (C) 2010 Google 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: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * 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. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT |
| * OWNER OR 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 "core/loader/PingLoader.h" |
| |
| #include "core/dom/ContextLifecycleObserver.h" |
| #include "core/dom/DOMArrayBufferView.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/SecurityContext.h" |
| #include "core/fileapi/File.h" |
| #include "core/frame/FrameConsole.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/frame/csp/ContentSecurityPolicy.h" |
| #include "core/html/FormData.h" |
| #include "core/inspector/ConsoleMessage.h" |
| #include "core/inspector/InspectorInstrumentation.h" |
| #include "core/inspector/InspectorTraceEvents.h" |
| #include "core/loader/FrameLoader.h" |
| #include "core/loader/FrameLoaderClient.h" |
| #include "core/loader/MixedContentChecker.h" |
| #include "core/page/Page.h" |
| #include "platform/WebFrameScheduler.h" |
| #include "platform/exported/WrappedResourceRequest.h" |
| #include "platform/exported/WrappedResourceResponse.h" |
| #include "platform/loader/fetch/CrossOriginAccessControl.h" |
| #include "platform/loader/fetch/FetchContext.h" |
| #include "platform/loader/fetch/FetchInitiatorTypeNames.h" |
| #include "platform/loader/fetch/FetchUtils.h" |
| #include "platform/loader/fetch/ResourceFetcher.h" |
| #include "platform/loader/fetch/UniqueIdentifier.h" |
| #include "platform/network/EncodedFormData.h" |
| #include "platform/network/ParsedContentType.h" |
| #include "platform/network/ResourceError.h" |
| #include "platform/network/ResourceRequest.h" |
| #include "platform/network/ResourceResponse.h" |
| #include "platform/weborigin/SecurityOrigin.h" |
| #include "platform/weborigin/SecurityPolicy.h" |
| #include "public/platform/Platform.h" |
| #include "public/platform/WebURLLoader.h" |
| #include "public/platform/WebURLRequest.h" |
| #include "public/platform/WebURLResponse.h" |
| #include "wtf/Compiler.h" |
| #include "wtf/Functional.h" |
| #include "wtf/PtrUtil.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| class Beacon { |
| STACK_ALLOCATED(); |
| |
| public: |
| virtual void serialize(ResourceRequest&) const = 0; |
| virtual unsigned long long size() const = 0; |
| virtual const AtomicString getContentType() const = 0; |
| }; |
| |
| class BeaconString final : public Beacon { |
| public: |
| explicit BeaconString(const String& data) : m_data(data) {} |
| |
| unsigned long long size() const override { |
| return m_data.charactersSizeInBytes(); |
| } |
| |
| void serialize(ResourceRequest& request) const override { |
| RefPtr<EncodedFormData> entityBody = EncodedFormData::create(m_data.utf8()); |
| request.setHTTPBody(entityBody); |
| request.setHTTPContentType(getContentType()); |
| } |
| |
| const AtomicString getContentType() const { |
| return AtomicString("text/plain;charset=UTF-8"); |
| } |
| |
| private: |
| const String m_data; |
| }; |
| |
| class BeaconBlob final : public Beacon { |
| public: |
| explicit BeaconBlob(Blob* data) : m_data(data) { |
| const String& blobType = m_data->type(); |
| if (!blobType.isEmpty() && isValidContentType(blobType)) |
| m_contentType = AtomicString(blobType); |
| } |
| |
| unsigned long long size() const override { return m_data->size(); } |
| |
| void serialize(ResourceRequest& request) const override { |
| DCHECK(m_data); |
| |
| RefPtr<EncodedFormData> entityBody = EncodedFormData::create(); |
| if (m_data->hasBackingFile()) |
| entityBody->appendFile(toFile(m_data)->path()); |
| else |
| entityBody->appendBlob(m_data->uuid(), m_data->blobDataHandle()); |
| |
| request.setHTTPBody(std::move(entityBody)); |
| |
| if (!m_contentType.isEmpty()) |
| request.setHTTPContentType(m_contentType); |
| } |
| |
| const AtomicString getContentType() const { return m_contentType; } |
| |
| private: |
| const Member<Blob> m_data; |
| AtomicString m_contentType; |
| }; |
| |
| class BeaconDOMArrayBufferView final : public Beacon { |
| public: |
| explicit BeaconDOMArrayBufferView(DOMArrayBufferView* data) : m_data(data) {} |
| |
| unsigned long long size() const override { return m_data->byteLength(); } |
| |
| void serialize(ResourceRequest& request) const override { |
| DCHECK(m_data); |
| |
| RefPtr<EncodedFormData> entityBody = |
| EncodedFormData::create(m_data->baseAddress(), m_data->byteLength()); |
| request.setHTTPBody(std::move(entityBody)); |
| |
| // FIXME: a reasonable choice, but not in the spec; should it give a |
| // default? |
| request.setHTTPContentType(AtomicString("application/octet-stream")); |
| } |
| |
| const AtomicString getContentType() const { return nullAtom; } |
| |
| private: |
| const Member<DOMArrayBufferView> m_data; |
| }; |
| |
| class BeaconFormData final : public Beacon { |
| public: |
| explicit BeaconFormData(FormData* data) |
| : m_data(data), m_entityBody(m_data->encodeMultiPartFormData()) { |
| m_contentType = AtomicString("multipart/form-data; boundary=") + |
| m_entityBody->boundary().data(); |
| } |
| |
| unsigned long long size() const override { |
| return m_entityBody->sizeInBytes(); |
| } |
| |
| void serialize(ResourceRequest& request) const override { |
| request.setHTTPBody(m_entityBody.get()); |
| request.setHTTPContentType(m_contentType); |
| } |
| |
| const AtomicString getContentType() const { return m_contentType; } |
| |
| private: |
| const Member<FormData> m_data; |
| RefPtr<EncodedFormData> m_entityBody; |
| AtomicString m_contentType; |
| }; |
| |
| class PingLoaderImpl : public GarbageCollectedFinalized<PingLoaderImpl>, |
| public ContextClient, |
| private WebURLLoaderClient { |
| USING_GARBAGE_COLLECTED_MIXIN(PingLoaderImpl); |
| WTF_MAKE_NONCOPYABLE(PingLoaderImpl); |
| |
| public: |
| PingLoaderImpl(LocalFrame*, |
| ResourceRequest&, |
| const AtomicString&, |
| StoredCredentials, |
| bool); |
| ~PingLoaderImpl() override; |
| |
| DECLARE_VIRTUAL_TRACE(); |
| |
| private: |
| void dispose(); |
| |
| // WebURLLoaderClient |
| bool willFollowRedirect(WebURLRequest&, const WebURLResponse&) override; |
| void didReceiveResponse(const WebURLResponse&) final; |
| void didReceiveData(const char*, int) final; |
| void didFinishLoading(double, int64_t, int64_t encodedDataLength) final; |
| void didFail(const WebURLError&, int64_t, int64_t encodedDataLength) final; |
| |
| void timeout(TimerBase*); |
| |
| void didFailLoading(LocalFrame*); |
| |
| std::unique_ptr<WebURLLoader> m_loader; |
| Timer<PingLoaderImpl> m_timeout; |
| String m_url; |
| unsigned long m_identifier; |
| SelfKeepAlive<PingLoaderImpl> m_keepAlive; |
| AtomicString m_initiator; |
| |
| bool m_isBeacon; |
| |
| RefPtr<SecurityOrigin> m_origin; |
| CORSEnabled m_corsMode; |
| }; |
| |
| PingLoaderImpl::PingLoaderImpl(LocalFrame* frame, |
| ResourceRequest& request, |
| const AtomicString& initiator, |
| StoredCredentials credentialsAllowed, |
| bool isBeacon) |
| : ContextClient(frame), |
| m_timeout(this, &PingLoaderImpl::timeout), |
| m_url(request.url()), |
| m_identifier(createUniqueIdentifier()), |
| m_keepAlive(this), |
| m_initiator(initiator), |
| m_isBeacon(isBeacon), |
| m_origin(frame->document()->getSecurityOrigin()), |
| m_corsMode(IsCORSEnabled) { |
| const AtomicString contentType = request.httpContentType(); |
| if (!contentType.isNull() && |
| FetchUtils::isSimpleHeader(AtomicString("content-type"), contentType)) |
| m_corsMode = NotCORSEnabled; |
| |
| frame->loader().client()->didDispatchPingLoader(request.url()); |
| |
| FetchContext& fetchContext = frame->document()->fetcher()->context(); |
| |
| fetchContext.willStartLoadingResource( |
| m_identifier, request, Resource::Image, initiator, |
| FetchContext::V8ActivityLoggingPolicy::Log); |
| |
| FetchInitiatorInfo initiatorInfo; |
| initiatorInfo.name = initiator; |
| fetchContext.dispatchWillSendRequest(m_identifier, request, |
| ResourceResponse(), initiatorInfo); |
| |
| // Make sure the scheduler doesn't wait for the ping. |
| if (frame->frameScheduler()) |
| frame->frameScheduler()->didStopLoading(m_identifier); |
| |
| m_loader = WTF::wrapUnique(Platform::current()->createURLLoader()); |
| DCHECK(m_loader); |
| WrappedResourceRequest wrappedRequest(request); |
| wrappedRequest.setAllowStoredCredentials(credentialsAllowed == |
| AllowStoredCredentials); |
| m_loader->loadAsynchronously(wrappedRequest, this); |
| |
| // If the server never responds, FrameLoader won't be able to cancel this load |
| // and we'll sit here waiting forever. Set a very generous timeout, just in |
| // case. |
| m_timeout.startOneShot(60000, BLINK_FROM_HERE); |
| } |
| |
| PingLoaderImpl::~PingLoaderImpl() { |
| if (m_loader) |
| m_loader->cancel(); |
| } |
| |
| void PingLoaderImpl::dispose() { |
| if (m_loader) { |
| m_loader->cancel(); |
| m_loader = nullptr; |
| } |
| m_timeout.stop(); |
| m_keepAlive.clear(); |
| } |
| |
| bool PingLoaderImpl::willFollowRedirect( |
| WebURLRequest& passedNewRequest, |
| const WebURLResponse& passedRedirectResponse) { |
| if (m_isBeacon && m_corsMode == IsCORSEnabled) { |
| DCHECK(passedNewRequest.allowStoredCredentials()); |
| |
| ResourceRequest& newRequest(passedNewRequest.toMutableResourceRequest()); |
| const ResourceResponse& redirectResponse( |
| passedRedirectResponse.toResourceResponse()); |
| |
| DCHECK(!newRequest.isNull()); |
| DCHECK(!redirectResponse.isNull()); |
| |
| String errorDescription; |
| ResourceLoaderOptions options; |
| // TODO(tyoshino): Save updated data in options.securityOrigin and pass it |
| // on the next time. |
| if (!CrossOriginAccessControl::handleRedirect( |
| m_origin, newRequest, redirectResponse, AllowStoredCredentials, |
| options, errorDescription)) { |
| if (frame()) { |
| if (frame()->document()) { |
| frame()->document()->addConsoleMessage(ConsoleMessage::create( |
| JSMessageSource, ErrorMessageLevel, errorDescription)); |
| } |
| } |
| // Cancel the load and self destruct. |
| dispose(); |
| |
| return false; |
| } |
| } |
| // FIXME: http://crbug.com/427429 is needed to correctly propagate updates of |
| // Origin: following this successful redirect. |
| |
| if (frame() && frame()->document()) { |
| FetchInitiatorInfo initiatorInfo; |
| initiatorInfo.name = m_initiator; |
| FetchContext& fetchContext = frame()->document()->fetcher()->context(); |
| fetchContext.dispatchWillSendRequest( |
| m_identifier, passedNewRequest.toMutableResourceRequest(), |
| passedRedirectResponse.toResourceResponse(), initiatorInfo); |
| } |
| |
| return true; |
| } |
| |
| void PingLoaderImpl::didReceiveResponse(const WebURLResponse& response) { |
| if (frame()) { |
| TRACE_EVENT1("devtools.timeline", "ResourceFinish", "data", |
| InspectorResourceFinishEvent::data(m_identifier, 0, true, 0)); |
| const ResourceResponse& resourceResponse = response.toResourceResponse(); |
| InspectorInstrumentation::didReceiveResourceResponse( |
| frame(), m_identifier, 0, resourceResponse, 0); |
| didFailLoading(frame()); |
| } |
| dispose(); |
| } |
| |
| void PingLoaderImpl::didReceiveData(const char*, int dataLength) { |
| if (frame()) { |
| TRACE_EVENT1( |
| "devtools.timeline", "ResourceFinish", "data", |
| InspectorResourceFinishEvent::data(m_identifier, 0, true, dataLength)); |
| didFailLoading(frame()); |
| } |
| dispose(); |
| } |
| |
| void PingLoaderImpl::didFinishLoading(double, |
| int64_t, |
| int64_t encodedDataLength) { |
| if (frame()) { |
| TRACE_EVENT1("devtools.timeline", "ResourceFinish", "data", |
| InspectorResourceFinishEvent::data(m_identifier, 0, true, |
| encodedDataLength)); |
| didFailLoading(frame()); |
| } |
| dispose(); |
| } |
| |
| void PingLoaderImpl::didFail(const WebURLError& resourceError, |
| int64_t, |
| int64_t encodedDataLength) { |
| if (frame()) { |
| TRACE_EVENT1("devtools.timeline", "ResourceFinish", "data", |
| InspectorResourceFinishEvent::data(m_identifier, 0, true, |
| encodedDataLength)); |
| didFailLoading(frame()); |
| } |
| dispose(); |
| } |
| |
| void PingLoaderImpl::timeout(TimerBase*) { |
| if (frame()) { |
| TRACE_EVENT1("devtools.timeline", "ResourceFinish", "data", |
| InspectorResourceFinishEvent::data(m_identifier, 0, true, 0)); |
| didFailLoading(frame()); |
| } |
| dispose(); |
| } |
| |
| void PingLoaderImpl::didFailLoading(LocalFrame* frame) { |
| InspectorInstrumentation::didFailLoading( |
| frame, m_identifier, ResourceError::cancelledError(m_url)); |
| frame->console().didFailLoading(m_identifier, |
| ResourceError::cancelledError(m_url)); |
| } |
| |
| DEFINE_TRACE(PingLoaderImpl) { |
| ContextClient::trace(visitor); |
| } |
| |
| void finishPingRequestInitialization( |
| ResourceRequest& request, |
| LocalFrame* frame, |
| WebURLRequest::RequestContext requestContext) { |
| request.setRequestContext(requestContext); |
| FetchContext& fetchContext = frame->document()->fetcher()->context(); |
| fetchContext.addAdditionalRequestHeaders(request, FetchSubresource); |
| // TODO(tyoshino): Call populateResourceRequest() if appropriate. |
| fetchContext.setFirstPartyCookieAndRequestorOrigin(request); |
| } |
| |
| bool sendPingCommon(LocalFrame* frame, |
| ResourceRequest& request, |
| const AtomicString& initiator, |
| StoredCredentials credentialsAllowed, |
| bool isBeacon) { |
| if (MixedContentChecker::shouldBlockFetch(frame, request, request.url())) |
| return false; |
| |
| // The loader keeps itself alive until it receives a response and disposes |
| // itself. |
| PingLoaderImpl* loader = |
| new PingLoaderImpl(frame, request, initiator, credentialsAllowed, true); |
| DCHECK(loader); |
| |
| return true; |
| } |
| |
| bool sendBeaconCommon(LocalFrame* frame, |
| int allowance, |
| const KURL& url, |
| const Beacon& beacon, |
| int& payloadLength) { |
| if (!frame->document()) |
| return false; |
| |
| unsigned long long entitySize = beacon.size(); |
| if (allowance > 0 && static_cast<unsigned long long>(allowance) < entitySize) |
| return false; |
| |
| payloadLength = entitySize; |
| |
| ResourceRequest request(url); |
| request.setHTTPMethod(HTTPNames::POST); |
| request.setHTTPHeaderField(HTTPNames::Cache_Control, "max-age=0"); |
| finishPingRequestInitialization(request, frame, |
| WebURLRequest::RequestContextBeacon); |
| |
| beacon.serialize(request); |
| |
| return sendPingCommon(frame, request, FetchInitiatorTypeNames::beacon, |
| AllowStoredCredentials, true); |
| } |
| |
| } // namespace |
| |
| void PingLoader::loadImage(LocalFrame* frame, const KURL& url) { |
| if (!frame->document()->getSecurityOrigin()->canDisplay(url)) { |
| FrameLoader::reportLocalLoadFailed(frame, url.getString()); |
| return; |
| } |
| |
| ResourceRequest request(url); |
| request.setHTTPHeaderField(HTTPNames::Cache_Control, "max-age=0"); |
| finishPingRequestInitialization(request, frame, |
| WebURLRequest::RequestContextPing); |
| |
| sendPingCommon(frame, request, FetchInitiatorTypeNames::ping, |
| AllowStoredCredentials, false); |
| } |
| |
| // http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#hyperlink-auditing |
| void PingLoader::sendLinkAuditPing(LocalFrame* frame, |
| const KURL& pingURL, |
| const KURL& destinationURL) { |
| if (!pingURL.protocolIsInHTTPFamily()) |
| return; |
| |
| if (ContentSecurityPolicy* policy = |
| frame->securityContext()->contentSecurityPolicy()) { |
| if (!policy->allowConnectToSource(pingURL)) |
| return; |
| } |
| |
| ResourceRequest request(pingURL); |
| request.setHTTPMethod(HTTPNames::POST); |
| request.setHTTPContentType("text/ping"); |
| request.setHTTPBody(EncodedFormData::create("PING")); |
| request.setHTTPHeaderField(HTTPNames::Cache_Control, "max-age=0"); |
| finishPingRequestInitialization(request, frame, |
| WebURLRequest::RequestContextPing); |
| |
| // addAdditionalRequestHeaders() will have added a referrer for same origin |
| // requests, but the spec omits the referrer. |
| request.clearHTTPReferrer(); |
| |
| request.setHTTPHeaderField(HTTPNames::Ping_To, |
| AtomicString(destinationURL.getString())); |
| |
| RefPtr<SecurityOrigin> pingOrigin = SecurityOrigin::create(pingURL); |
| if (protocolIs(frame->document()->url().getString(), "http") || |
| frame->document()->getSecurityOrigin()->canAccess(pingOrigin.get())) { |
| request.setHTTPHeaderField( |
| HTTPNames::Ping_From, |
| AtomicString(frame->document()->url().getString())); |
| } |
| |
| sendPingCommon(frame, request, FetchInitiatorTypeNames::ping, |
| AllowStoredCredentials, false); |
| } |
| |
| void PingLoader::sendViolationReport(LocalFrame* frame, |
| const KURL& reportURL, |
| PassRefPtr<EncodedFormData> report, |
| ViolationReportType type) { |
| ResourceRequest request(reportURL); |
| request.setHTTPMethod(HTTPNames::POST); |
| request.setHTTPContentType(type == ContentSecurityPolicyViolationReport |
| ? "application/csp-report" |
| : "application/json"); |
| request.setHTTPBody(std::move(report)); |
| finishPingRequestInitialization(request, frame, |
| WebURLRequest::RequestContextPing); |
| |
| StoredCredentials credentialsAllowed = |
| SecurityOrigin::create(reportURL)->isSameSchemeHostPort( |
| frame->document()->getSecurityOrigin()) |
| ? AllowStoredCredentials |
| : DoNotAllowStoredCredentials; |
| sendPingCommon(frame, request, FetchInitiatorTypeNames::violationreport, |
| credentialsAllowed, false); |
| } |
| |
| bool PingLoader::sendBeacon(LocalFrame* frame, |
| int allowance, |
| const KURL& beaconURL, |
| const String& data, |
| int& payloadLength) { |
| BeaconString beacon(data); |
| return sendBeaconCommon(frame, allowance, beaconURL, beacon, payloadLength); |
| } |
| |
| bool PingLoader::sendBeacon(LocalFrame* frame, |
| int allowance, |
| const KURL& beaconURL, |
| DOMArrayBufferView* data, |
| int& payloadLength) { |
| BeaconDOMArrayBufferView beacon(data); |
| return sendBeaconCommon(frame, allowance, beaconURL, beacon, payloadLength); |
| } |
| |
| bool PingLoader::sendBeacon(LocalFrame* frame, |
| int allowance, |
| const KURL& beaconURL, |
| FormData* data, |
| int& payloadLength) { |
| BeaconFormData beacon(data); |
| return sendBeaconCommon(frame, allowance, beaconURL, beacon, payloadLength); |
| } |
| |
| bool PingLoader::sendBeacon(LocalFrame* frame, |
| int allowance, |
| const KURL& beaconURL, |
| Blob* data, |
| int& payloadLength) { |
| BeaconBlob beacon(data); |
| return sendBeaconCommon(frame, allowance, beaconURL, beacon, payloadLength); |
| } |
| |
| } // namespace blink |