1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "nsPingListener.h"
9 #include "mozilla/Encoding.h"
10 #include "mozilla/Preferences.h"
12 #include "mozilla/dom/DocGroup.h"
13 #include "mozilla/dom/Document.h"
15 #include "nsIHttpChannel.h"
16 #include "nsIHttpChannelInternal.h"
17 #include "nsIInputStream.h"
18 #include "nsIProtocolHandler.h"
19 #include "nsIUploadChannel2.h"
21 #include "nsComponentManagerUtils.h"
22 #include "nsNetUtil.h"
23 #include "nsStreamUtils.h"
24 #include "nsStringStream.h"
25 #include "nsWhitespaceTokenizer.h"
27 using namespace mozilla
;
28 using namespace mozilla::dom
;
30 NS_IMPL_ISUPPORTS(nsPingListener
, nsIStreamListener
, nsIRequestObserver
)
32 //*****************************************************************************
34 //*****************************************************************************
36 #define PREF_PINGS_ENABLED "browser.send_pings"
37 #define PREF_PINGS_MAX_PER_LINK "browser.send_pings.max_per_link"
38 #define PREF_PINGS_REQUIRE_SAME_HOST "browser.send_pings.require_same_host"
40 // Check prefs to see if pings are enabled and if so what restrictions might
44 // This parameter returns the number of pings that are allowed per link click
46 // @param requireSameHost
47 // This parameter returns true if pings are restricted to the same host as
48 // the document in which the click occurs. If the same host restriction is
49 // imposed, then we still allow for pings to cross over to different
50 // protocols and ports for flexibility and because it is not possible to send
54 // true if pings are enabled and false otherwise.
56 static bool PingsEnabled(int32_t* aMaxPerLink
, bool* aRequireSameHost
) {
57 bool allow
= Preferences::GetBool(PREF_PINGS_ENABLED
, false);
60 *aRequireSameHost
= true;
63 Preferences::GetInt(PREF_PINGS_MAX_PER_LINK
, aMaxPerLink
);
64 Preferences::GetBool(PREF_PINGS_REQUIRE_SAME_HOST
, aRequireSameHost
);
70 // We wait this many milliseconds before killing the ping channel...
71 #define PING_TIMEOUT 10000
73 static void OnPingTimeout(nsITimer
* aTimer
, void* aClosure
) {
74 nsILoadGroup
* loadGroup
= static_cast<nsILoadGroup
*>(aClosure
);
76 loadGroup
->Cancel(NS_ERROR_ABORT
);
80 struct MOZ_STACK_CLASS SendPingInfo
{
85 nsIReferrerInfo
* referrerInfo
;
86 nsIDocShell
* docShell
;
89 static void SendPing(void* aClosure
, nsIContent
* aContent
, nsIURI
* aURI
,
90 nsIIOService
* aIOService
) {
91 SendPingInfo
* info
= static_cast<SendPingInfo
*>(aClosure
);
92 if (info
->maxPings
> -1 && info
->numPings
>= info
->maxPings
) {
96 Document
* doc
= aContent
->OwnerDoc();
98 nsCOMPtr
<nsIChannel
> chan
;
99 NS_NewChannel(getter_AddRefs(chan
), aURI
, doc
,
100 info
->requireSameHost
101 ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED
102 : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL
,
103 nsIContentPolicy::TYPE_PING
,
104 nullptr, // PerformanceStorage
105 nullptr, // aLoadGroup
106 nullptr, // aCallbacks
107 nsIRequest::LOAD_NORMAL
, // aLoadFlags,
114 // Don't bother caching the result of this URI load, but do not exempt
115 // it from Safe Browsing.
116 chan
->SetLoadFlags(nsIRequest::INHIBIT_CACHING
);
118 nsCOMPtr
<nsIHttpChannel
> httpChan
= do_QueryInterface(chan
);
123 // This is needed in order for 3rd-party cookie blocking to work.
124 nsCOMPtr
<nsIHttpChannelInternal
> httpInternal
= do_QueryInterface(httpChan
);
127 rv
= httpInternal
->SetDocumentURI(doc
->GetDocumentURI());
128 MOZ_ASSERT(NS_SUCCEEDED(rv
));
131 rv
= httpChan
->SetRequestMethod("POST"_ns
);
132 MOZ_ASSERT(NS_SUCCEEDED(rv
));
134 // Remove extraneous request headers (to reduce request size)
135 rv
= httpChan
->SetRequestHeader("accept"_ns
, ""_ns
, false);
136 MOZ_ASSERT(NS_SUCCEEDED(rv
));
137 rv
= httpChan
->SetRequestHeader("accept-language"_ns
, ""_ns
, false);
138 MOZ_ASSERT(NS_SUCCEEDED(rv
));
139 rv
= httpChan
->SetRequestHeader("accept-encoding"_ns
, ""_ns
, false);
140 MOZ_ASSERT(NS_SUCCEEDED(rv
));
142 // Always send a Ping-To header.
143 nsAutoCString pingTo
;
144 if (NS_SUCCEEDED(info
->target
->GetSpec(pingTo
))) {
145 rv
= httpChan
->SetRequestHeader("Ping-To"_ns
, pingTo
, false);
146 MOZ_ASSERT(NS_SUCCEEDED(rv
));
149 nsCOMPtr
<nsIScriptSecurityManager
> sm
=
150 do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID
);
152 if (sm
&& info
->referrerInfo
) {
153 nsCOMPtr
<nsIURI
> referrer
= info
->referrerInfo
->GetOriginalReferrer();
154 bool referrerIsSecure
= false;
155 uint32_t flags
= nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY
;
157 rv
= NS_URIChainHasFlags(referrer
, flags
, &referrerIsSecure
);
160 // Default to sending less data if NS_URIChainHasFlags() fails.
161 referrerIsSecure
= NS_FAILED(rv
) || referrerIsSecure
;
163 bool isPrivateWin
= false;
166 doc
->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId
> 0;
169 bool sameOrigin
= NS_SUCCEEDED(
170 sm
->CheckSameOriginURI(referrer
, aURI
, false, isPrivateWin
));
172 // If both the address of the document containing the hyperlink being
173 // audited and "ping URL" have the same origin or the document containing
174 // the hyperlink being audited was not retrieved over an encrypted
175 // connection, send a Ping-From header.
176 if (sameOrigin
|| !referrerIsSecure
) {
177 nsAutoCString pingFrom
;
178 if (NS_SUCCEEDED(referrer
->GetSpec(pingFrom
))) {
179 rv
= httpChan
->SetRequestHeader("Ping-From"_ns
, pingFrom
, false);
180 MOZ_ASSERT(NS_SUCCEEDED(rv
));
184 // If the document containing the hyperlink being audited was not retrieved
185 // over an encrypted connection and its address does not have the same
186 // origin as "ping URL", send a referrer.
187 if (!sameOrigin
&& !referrerIsSecure
&& info
->referrerInfo
) {
188 rv
= httpChan
->SetReferrerInfo(info
->referrerInfo
);
189 MOZ_ASSERT(NS_SUCCEEDED(rv
));
193 nsCOMPtr
<nsIUploadChannel2
> uploadChan
= do_QueryInterface(httpChan
);
198 constexpr auto uploadData
= "PING"_ns
;
200 nsCOMPtr
<nsIInputStream
> uploadStream
;
201 rv
= NS_NewCStringInputStream(getter_AddRefs(uploadStream
), uploadData
);
202 if (NS_WARN_IF(NS_FAILED(rv
))) {
206 uploadChan
->ExplicitSetUploadStream(uploadStream
, "text/ping"_ns
,
207 uploadData
.Length(), "POST"_ns
, false);
209 // The channel needs to have a loadgroup associated with it, so that we can
210 // cancel the channel and any redirected channels it may create.
211 nsCOMPtr
<nsILoadGroup
> loadGroup
= do_CreateInstance(NS_LOADGROUP_CONTRACTID
);
215 nsCOMPtr
<nsIInterfaceRequestor
> callbacks
= do_QueryInterface(info
->docShell
);
216 loadGroup
->SetNotificationCallbacks(callbacks
);
217 chan
->SetLoadGroup(loadGroup
);
219 RefPtr
<nsPingListener
> pingListener
= new nsPingListener();
220 chan
->AsyncOpen(pingListener
);
222 // Even if AsyncOpen failed, we still count this as a successful ping. It's
223 // possible that AsyncOpen may have failed after triggering some background
224 // process that may have written something to the network.
227 // Prevent ping requests from stalling and never being garbage collected...
228 if (NS_FAILED(pingListener
->StartTimeout(doc
->GetDocGroup()))) {
229 // If we failed to setup the timer, then we should just cancel the channel
230 // because we won't be able to ensure that it goes away in a timely manner.
231 chan
->Cancel(NS_ERROR_ABORT
);
234 // if the channel openend successfully, then make the pingListener hold
235 // a strong reference to the loadgroup which is released in ::OnStopRequest
236 pingListener
->SetLoadGroup(loadGroup
);
239 typedef void (*ForEachPingCallback
)(void* closure
, nsIContent
* content
,
240 nsIURI
* uri
, nsIIOService
* ios
);
242 static void ForEachPing(nsIContent
* aContent
, ForEachPingCallback aCallback
,
244 // NOTE: Using nsIDOMHTMLAnchorElement::GetPing isn't really worth it here
245 // since we'd still need to parse the resulting string. Instead, we
246 // just parse the raw attribute. It might be nice if the content node
247 // implemented an interface that exposed an enumeration of nsIURIs.
249 // Make sure we are dealing with either an <A> or <AREA> element in the HTML
250 // or XHTML namespace.
251 if (!aContent
->IsAnyOfHTMLElements(nsGkAtoms::a
, nsGkAtoms::area
)) {
256 aContent
->AsElement()->GetAttr(nsGkAtoms::ping
, value
);
257 if (value
.IsEmpty()) {
261 nsCOMPtr
<nsIIOService
> ios
= do_GetIOService();
266 Document
* doc
= aContent
->OwnerDoc();
267 nsAutoCString charset
;
268 doc
->GetDocumentCharacterSet()->Name(charset
);
270 nsWhitespaceTokenizer
tokenizer(value
);
272 while (tokenizer
.hasMoreTokens()) {
273 nsCOMPtr
<nsIURI
> uri
;
274 NS_NewURI(getter_AddRefs(uri
), tokenizer
.nextToken(), charset
.get(),
275 aContent
->GetBaseURI());
276 // if we can't generate a valid URI, then there is nothing to do
280 // Explicitly not allow loading data: URIs
281 if (!net::SchemeIsData(uri
)) {
282 aCallback(aClosure
, aContent
, uri
, ios
);
287 // Spec: http://whatwg.org/specs/web-apps/current-work/#ping
288 /*static*/ void nsPingListener::DispatchPings(nsIDocShell
* aDocShell
,
289 nsIContent
* aContent
,
291 nsIReferrerInfo
* aReferrerInfo
) {
294 if (!PingsEnabled(&info
.maxPings
, &info
.requireSameHost
)) {
297 if (info
.maxPings
== 0) {
302 info
.target
= aTarget
;
303 info
.referrerInfo
= aReferrerInfo
;
304 info
.docShell
= aDocShell
;
306 ForEachPing(aContent
, SendPing
, &info
);
309 nsPingListener::~nsPingListener() {
316 nsresult
nsPingListener::StartTimeout(DocGroup
* aDocGroup
) {
317 NS_ENSURE_ARG(aDocGroup
);
319 return NS_NewTimerWithFuncCallback(
320 getter_AddRefs(mTimer
), OnPingTimeout
, mLoadGroup
, PING_TIMEOUT
,
321 nsITimer::TYPE_ONE_SHOT
, "nsPingListener::StartTimeout",
322 GetMainThreadSerialEventTarget());
326 nsPingListener::OnStartRequest(nsIRequest
* aRequest
) { return NS_OK
; }
329 nsPingListener::OnDataAvailable(nsIRequest
* aRequest
, nsIInputStream
* aStream
,
330 uint64_t aOffset
, uint32_t aCount
) {
332 return aStream
->ReadSegments(NS_DiscardSegment
, nullptr, aCount
, &result
);
336 nsPingListener::OnStopRequest(nsIRequest
* aRequest
, nsresult aStatus
) {
337 mLoadGroup
= nullptr;