Bug 1861709 replace AudioCallbackDriver::ThreadRunning() assertions that mean to...
[gecko.git] / docshell / base / nsPingListener.cpp
blob094074a0b9e5cdf6a3c3ece008981cf55eddae6b
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 //*****************************************************************************
33 // <a ping> support
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
41 // be applied.
43 // @param maxPerLink
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
51 // a ping via FTP.
53 // @returns
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);
59 *aMaxPerLink = 1;
60 *aRequireSameHost = true;
62 if (allow) {
63 Preferences::GetInt(PREF_PINGS_MAX_PER_LINK, aMaxPerLink);
64 Preferences::GetBool(PREF_PINGS_REQUIRE_SAME_HOST, aRequireSameHost);
67 return allow;
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);
75 if (loadGroup) {
76 loadGroup->Cancel(NS_ERROR_ABORT);
80 struct MOZ_STACK_CLASS SendPingInfo {
81 int32_t numPings;
82 int32_t maxPings;
83 bool requireSameHost;
84 nsIURI* target;
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) {
93 return;
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,
108 aIOService);
110 if (!chan) {
111 return;
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);
119 if (!httpChan) {
120 return;
123 // This is needed in order for 3rd-party cookie blocking to work.
124 nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(httpChan);
125 nsresult rv;
126 if (httpInternal) {
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;
156 if (referrer) {
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;
164 if (doc) {
165 isPrivateWin =
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);
194 if (!uploadChan) {
195 return;
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))) {
203 return;
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);
212 if (!loadGroup) {
213 return;
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.
225 info->numPings++;
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);
232 return;
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,
243 void* aClosure) {
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)) {
252 return;
255 nsAutoString value;
256 aContent->AsElement()->GetAttr(nsGkAtoms::ping, value);
257 if (value.IsEmpty()) {
258 return;
261 nsCOMPtr<nsIIOService> ios = do_GetIOService();
262 if (!ios) {
263 return;
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
277 if (!uri) {
278 continue;
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,
290 nsIURI* aTarget,
291 nsIReferrerInfo* aReferrerInfo) {
292 SendPingInfo info;
294 if (!PingsEnabled(&info.maxPings, &info.requireSameHost)) {
295 return;
297 if (info.maxPings == 0) {
298 return;
301 info.numPings = 0;
302 info.target = aTarget;
303 info.referrerInfo = aReferrerInfo;
304 info.docShell = aDocShell;
306 ForEachPing(aContent, SendPing, &info);
309 nsPingListener::~nsPingListener() {
310 if (mTimer) {
311 mTimer->Cancel();
312 mTimer = nullptr;
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());
325 NS_IMETHODIMP
326 nsPingListener::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }
328 NS_IMETHODIMP
329 nsPingListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
330 uint64_t aOffset, uint32_t aCount) {
331 uint32_t result;
332 return aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &result);
335 NS_IMETHODIMP
336 nsPingListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
337 mLoadGroup = nullptr;
339 if (mTimer) {
340 mTimer->Cancel();
341 mTimer = nullptr;
344 return NS_OK;