summaryrefslogtreecommitdiffstats
path: root/docshell/base/nsPingListener.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'docshell/base/nsPingListener.cpp')
-rw-r--r--docshell/base/nsPingListener.cpp345
1 files changed, 345 insertions, 0 deletions
diff --git a/docshell/base/nsPingListener.cpp b/docshell/base/nsPingListener.cpp
new file mode 100644
index 0000000000..23ac220489
--- /dev/null
+++ b/docshell/base/nsPingListener.cpp
@@ -0,0 +1,345 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsPingListener.h"
+
+#include "mozilla/Encoding.h"
+#include "mozilla/Preferences.h"
+
+#include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/Document.h"
+
+#include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsIInputStream.h"
+#include "nsIProtocolHandler.h"
+#include "nsIUploadChannel2.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+#include "nsStringStream.h"
+#include "nsWhitespaceTokenizer.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+NS_IMPL_ISUPPORTS(nsPingListener, nsIStreamListener, nsIRequestObserver)
+
+//*****************************************************************************
+// <a ping> support
+//*****************************************************************************
+
+#define PREF_PINGS_ENABLED "browser.send_pings"
+#define PREF_PINGS_MAX_PER_LINK "browser.send_pings.max_per_link"
+#define PREF_PINGS_REQUIRE_SAME_HOST "browser.send_pings.require_same_host"
+
+// Check prefs to see if pings are enabled and if so what restrictions might
+// be applied.
+//
+// @param maxPerLink
+// This parameter returns the number of pings that are allowed per link click
+//
+// @param requireSameHost
+// This parameter returns true if pings are restricted to the same host as
+// the document in which the click occurs. If the same host restriction is
+// imposed, then we still allow for pings to cross over to different
+// protocols and ports for flexibility and because it is not possible to send
+// a ping via FTP.
+//
+// @returns
+// true if pings are enabled and false otherwise.
+//
+static bool PingsEnabled(int32_t* aMaxPerLink, bool* aRequireSameHost) {
+ bool allow = Preferences::GetBool(PREF_PINGS_ENABLED, false);
+
+ *aMaxPerLink = 1;
+ *aRequireSameHost = true;
+
+ if (allow) {
+ Preferences::GetInt(PREF_PINGS_MAX_PER_LINK, aMaxPerLink);
+ Preferences::GetBool(PREF_PINGS_REQUIRE_SAME_HOST, aRequireSameHost);
+ }
+
+ return allow;
+}
+
+// We wait this many milliseconds before killing the ping channel...
+#define PING_TIMEOUT 10000
+
+static void OnPingTimeout(nsITimer* aTimer, void* aClosure) {
+ nsILoadGroup* loadGroup = static_cast<nsILoadGroup*>(aClosure);
+ if (loadGroup) {
+ loadGroup->Cancel(NS_ERROR_ABORT);
+ }
+}
+
+struct MOZ_STACK_CLASS SendPingInfo {
+ int32_t numPings;
+ int32_t maxPings;
+ bool requireSameHost;
+ nsIURI* target;
+ nsIReferrerInfo* referrerInfo;
+ nsIDocShell* docShell;
+};
+
+static void SendPing(void* aClosure, nsIContent* aContent, nsIURI* aURI,
+ nsIIOService* aIOService) {
+ SendPingInfo* info = static_cast<SendPingInfo*>(aClosure);
+ if (info->maxPings > -1 && info->numPings >= info->maxPings) {
+ return;
+ }
+
+ Document* doc = aContent->OwnerDoc();
+
+ nsCOMPtr<nsIChannel> chan;
+ NS_NewChannel(getter_AddRefs(chan), aURI, doc,
+ info->requireSameHost
+ ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED
+ : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_PING,
+ nullptr, // PerformanceStorage
+ nullptr, // aLoadGroup
+ nullptr, // aCallbacks
+ nsIRequest::LOAD_NORMAL, // aLoadFlags,
+ aIOService);
+
+ if (!chan) {
+ return;
+ }
+
+ // Don't bother caching the result of this URI load, but do not exempt
+ // it from Safe Browsing.
+ chan->SetLoadFlags(nsIRequest::INHIBIT_CACHING);
+
+ nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan);
+ if (!httpChan) {
+ return;
+ }
+
+ // This is needed in order for 3rd-party cookie blocking to work.
+ nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(httpChan);
+ nsresult rv;
+ if (httpInternal) {
+ rv = httpInternal->SetDocumentURI(doc->GetDocumentURI());
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ rv = httpChan->SetRequestMethod("POST"_ns);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Remove extraneous request headers (to reduce request size)
+ rv = httpChan->SetRequestHeader("accept"_ns, ""_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = httpChan->SetRequestHeader("accept-language"_ns, ""_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = httpChan->SetRequestHeader("accept-encoding"_ns, ""_ns, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ // Always send a Ping-To header.
+ nsAutoCString pingTo;
+ if (NS_SUCCEEDED(info->target->GetSpec(pingTo))) {
+ rv = httpChan->SetRequestHeader("Ping-To"_ns, pingTo, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ nsCOMPtr<nsIScriptSecurityManager> sm =
+ do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
+
+ if (sm && info->referrerInfo) {
+ nsCOMPtr<nsIURI> referrer = info->referrerInfo->GetOriginalReferrer();
+ bool referrerIsSecure = false;
+ uint32_t flags = nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY;
+ if (referrer) {
+ rv = NS_URIChainHasFlags(referrer, flags, &referrerIsSecure);
+ }
+
+ // Default to sending less data if NS_URIChainHasFlags() fails.
+ referrerIsSecure = NS_FAILED(rv) || referrerIsSecure;
+
+ bool isPrivateWin = false;
+ if (doc) {
+ isPrivateWin =
+ doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId > 0;
+ }
+
+ bool sameOrigin = NS_SUCCEEDED(
+ sm->CheckSameOriginURI(referrer, aURI, false, isPrivateWin));
+
+ // If both the address of the document containing the hyperlink being
+ // audited and "ping URL" have the same origin or the document containing
+ // the hyperlink being audited was not retrieved over an encrypted
+ // connection, send a Ping-From header.
+ if (sameOrigin || !referrerIsSecure) {
+ nsAutoCString pingFrom;
+ if (NS_SUCCEEDED(referrer->GetSpec(pingFrom))) {
+ rv = httpChan->SetRequestHeader("Ping-From"_ns, pingFrom, false);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+
+ // If the document containing the hyperlink being audited was not retrieved
+ // over an encrypted connection and its address does not have the same
+ // origin as "ping URL", send a referrer.
+ if (!sameOrigin && !referrerIsSecure && info->referrerInfo) {
+ rv = httpChan->SetReferrerInfo(info->referrerInfo);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+
+ nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(httpChan);
+ if (!uploadChan) {
+ return;
+ }
+
+ constexpr auto uploadData = "PING"_ns;
+
+ nsCOMPtr<nsIInputStream> uploadStream;
+ rv = NS_NewCStringInputStream(getter_AddRefs(uploadStream), uploadData);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ uploadChan->ExplicitSetUploadStream(uploadStream, "text/ping"_ns,
+ uploadData.Length(), "POST"_ns, false);
+
+ // The channel needs to have a loadgroup associated with it, so that we can
+ // cancel the channel and any redirected channels it may create.
+ nsCOMPtr<nsILoadGroup> loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID);
+ if (!loadGroup) {
+ return;
+ }
+ nsCOMPtr<nsIInterfaceRequestor> callbacks = do_QueryInterface(info->docShell);
+ loadGroup->SetNotificationCallbacks(callbacks);
+ chan->SetLoadGroup(loadGroup);
+
+ RefPtr<nsPingListener> pingListener = new nsPingListener();
+ chan->AsyncOpen(pingListener);
+
+ // Even if AsyncOpen failed, we still count this as a successful ping. It's
+ // possible that AsyncOpen may have failed after triggering some background
+ // process that may have written something to the network.
+ info->numPings++;
+
+ // Prevent ping requests from stalling and never being garbage collected...
+ if (NS_FAILED(pingListener->StartTimeout(doc->GetDocGroup()))) {
+ // If we failed to setup the timer, then we should just cancel the channel
+ // because we won't be able to ensure that it goes away in a timely manner.
+ chan->Cancel(NS_ERROR_ABORT);
+ return;
+ }
+ // if the channel openend successfully, then make the pingListener hold
+ // a strong reference to the loadgroup which is released in ::OnStopRequest
+ pingListener->SetLoadGroup(loadGroup);
+}
+
+typedef void (*ForEachPingCallback)(void* closure, nsIContent* content,
+ nsIURI* uri, nsIIOService* ios);
+
+static void ForEachPing(nsIContent* aContent, ForEachPingCallback aCallback,
+ void* aClosure) {
+ // NOTE: Using nsIDOMHTMLAnchorElement::GetPing isn't really worth it here
+ // since we'd still need to parse the resulting string. Instead, we
+ // just parse the raw attribute. It might be nice if the content node
+ // implemented an interface that exposed an enumeration of nsIURIs.
+
+ // Make sure we are dealing with either an <A> or <AREA> element in the HTML
+ // or XHTML namespace.
+ if (!aContent->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area)) {
+ return;
+ }
+
+ nsAutoString value;
+ aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ping, value);
+ if (value.IsEmpty()) {
+ return;
+ }
+
+ nsCOMPtr<nsIIOService> ios = do_GetIOService();
+ if (!ios) {
+ return;
+ }
+
+ Document* doc = aContent->OwnerDoc();
+ nsAutoCString charset;
+ doc->GetDocumentCharacterSet()->Name(charset);
+
+ nsWhitespaceTokenizer tokenizer(value);
+
+ while (tokenizer.hasMoreTokens()) {
+ nsCOMPtr<nsIURI> uri;
+ NS_NewURI(getter_AddRefs(uri), tokenizer.nextToken(), charset.get(),
+ aContent->GetBaseURI());
+ // if we can't generate a valid URI, then there is nothing to do
+ if (!uri) {
+ continue;
+ }
+ // Explicitly not allow loading data: URIs
+ if (!net::SchemeIsData(uri)) {
+ aCallback(aClosure, aContent, uri, ios);
+ }
+ }
+}
+
+// Spec: http://whatwg.org/specs/web-apps/current-work/#ping
+/*static*/ void nsPingListener::DispatchPings(nsIDocShell* aDocShell,
+ nsIContent* aContent,
+ nsIURI* aTarget,
+ nsIReferrerInfo* aReferrerInfo) {
+ SendPingInfo info;
+
+ if (!PingsEnabled(&info.maxPings, &info.requireSameHost)) {
+ return;
+ }
+ if (info.maxPings == 0) {
+ return;
+ }
+
+ info.numPings = 0;
+ info.target = aTarget;
+ info.referrerInfo = aReferrerInfo;
+ info.docShell = aDocShell;
+
+ ForEachPing(aContent, SendPing, &info);
+}
+
+nsPingListener::~nsPingListener() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+nsresult nsPingListener::StartTimeout(DocGroup* aDocGroup) {
+ NS_ENSURE_ARG(aDocGroup);
+
+ return NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mTimer), OnPingTimeout, mLoadGroup, PING_TIMEOUT,
+ nsITimer::TYPE_ONE_SHOT, "nsPingListener::StartTimeout",
+ aDocGroup->EventTargetFor(TaskCategory::Network));
+}
+
+NS_IMETHODIMP
+nsPingListener::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }
+
+NS_IMETHODIMP
+nsPingListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
+ uint64_t aOffset, uint32_t aCount) {
+ uint32_t result;
+ return aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &result);
+}
+
+NS_IMETHODIMP
+nsPingListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ mLoadGroup = nullptr;
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+
+ return NS_OK;
+}