nsPingListener.cpp (11692B)
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/. */ 6 7 #include "nsPingListener.h" 8 9 #include "mozilla/Encoding.h" 10 #include "mozilla/Preferences.h" 11 12 #include "mozilla/dom/DocGroup.h" 13 #include "mozilla/dom/Document.h" 14 15 #include "nsIHttpChannel.h" 16 #include "nsIHttpChannelInternal.h" 17 #include "nsIInputStream.h" 18 #include "nsIProtocolHandler.h" 19 #include "nsIUploadChannel2.h" 20 21 #include "nsComponentManagerUtils.h" 22 #include "nsNetUtil.h" 23 #include "nsStreamUtils.h" 24 #include "nsStringStream.h" 25 #include "nsWhitespaceTokenizer.h" 26 27 using namespace mozilla; 28 using namespace mozilla::dom; 29 30 NS_IMPL_ISUPPORTS(nsPingListener, nsIStreamListener, nsIRequestObserver) 31 32 //***************************************************************************** 33 // <a ping> support 34 //***************************************************************************** 35 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" 39 40 // Check prefs to see if pings are enabled and if so what restrictions might 41 // be applied. 42 // 43 // @param maxPerLink 44 // This parameter returns the number of pings that are allowed per link click 45 // 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. 52 // 53 // @returns 54 // true if pings are enabled and false otherwise. 55 // 56 static bool PingsEnabled(int32_t* aMaxPerLink, bool* aRequireSameHost) { 57 bool allow = Preferences::GetBool(PREF_PINGS_ENABLED, false); 58 59 *aMaxPerLink = 1; 60 *aRequireSameHost = true; 61 62 if (allow) { 63 Preferences::GetInt(PREF_PINGS_MAX_PER_LINK, aMaxPerLink); 64 Preferences::GetBool(PREF_PINGS_REQUIRE_SAME_HOST, aRequireSameHost); 65 } 66 67 return allow; 68 } 69 70 // We wait this many milliseconds before killing the ping channel... 71 #define PING_TIMEOUT 10000 72 73 static void OnPingTimeout(nsITimer* aTimer, void* aClosure) { 74 nsILoadGroup* loadGroup = static_cast<nsILoadGroup*>(aClosure); 75 if (loadGroup) { 76 loadGroup->Cancel(NS_ERROR_ABORT); 77 } 78 } 79 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; 87 }; 88 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; 94 } 95 96 Document* doc = aContent->OwnerDoc(); 97 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); 109 110 if (!chan) { 111 return; 112 } 113 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); 117 118 nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan); 119 if (!httpChan) { 120 return; 121 } 122 123 if (nsCOMPtr<nsITimedChannel> timedChan = do_QueryInterface(chan)) { 124 timedChan->SetInitiatorType(u"ping"_ns); 125 } 126 127 // This is needed in order for 3rd-party cookie blocking to work. 128 nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(httpChan); 129 nsresult rv; 130 if (httpInternal) { 131 rv = httpInternal->SetDocumentURI(doc->GetDocumentURI()); 132 MOZ_ASSERT(NS_SUCCEEDED(rv)); 133 } 134 135 rv = httpChan->SetRequestMethod("POST"_ns); 136 MOZ_ASSERT(NS_SUCCEEDED(rv)); 137 138 // Remove extraneous request headers (to reduce request size) 139 rv = httpChan->SetRequestHeader("accept"_ns, ""_ns, false); 140 MOZ_ASSERT(NS_SUCCEEDED(rv)); 141 rv = httpChan->SetRequestHeader("accept-language"_ns, ""_ns, false); 142 MOZ_ASSERT(NS_SUCCEEDED(rv)); 143 rv = httpChan->SetRequestHeader("accept-encoding"_ns, ""_ns, false); 144 MOZ_ASSERT(NS_SUCCEEDED(rv)); 145 146 // Always send a Ping-To header. 147 nsAutoCString pingTo; 148 if (NS_SUCCEEDED(info->target->GetSpec(pingTo))) { 149 rv = httpChan->SetRequestHeader("Ping-To"_ns, pingTo, false); 150 MOZ_ASSERT(NS_SUCCEEDED(rv)); 151 } 152 153 nsCOMPtr<nsIScriptSecurityManager> sm = 154 do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID); 155 156 if (sm && info->referrerInfo) { 157 nsCOMPtr<nsIURI> referrer = info->referrerInfo->GetOriginalReferrer(); 158 bool referrerIsSecure = false; 159 uint32_t flags = nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY; 160 if (referrer) { 161 rv = NS_URIChainHasFlags(referrer, flags, &referrerIsSecure); 162 } 163 164 // Default to sending less data if NS_URIChainHasFlags() fails. 165 referrerIsSecure = NS_FAILED(rv) || referrerIsSecure; 166 167 bool isPrivateWin = false; 168 if (doc) { 169 isPrivateWin = 170 doc->NodePrincipal()->OriginAttributesRef().IsPrivateBrowsing(); 171 } 172 173 bool sameOrigin = NS_SUCCEEDED( 174 sm->CheckSameOriginURI(referrer, aURI, false, isPrivateWin)); 175 176 // If both the address of the document containing the hyperlink being 177 // audited and "ping URL" have the same origin or the document containing 178 // the hyperlink being audited was not retrieved over an encrypted 179 // connection, send a Ping-From header. 180 if (sameOrigin || !referrerIsSecure) { 181 nsAutoCString pingFrom; 182 if (NS_SUCCEEDED(referrer->GetSpec(pingFrom))) { 183 rv = httpChan->SetRequestHeader("Ping-From"_ns, pingFrom, false); 184 MOZ_ASSERT(NS_SUCCEEDED(rv)); 185 } 186 } 187 188 // If the document containing the hyperlink being audited was not retrieved 189 // over an encrypted connection and its address does not have the same 190 // origin as "ping URL", send a referrer. 191 if (!sameOrigin && !referrerIsSecure && info->referrerInfo) { 192 rv = httpChan->SetReferrerInfo(info->referrerInfo); 193 MOZ_ASSERT(NS_SUCCEEDED(rv)); 194 } 195 } 196 197 nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(httpChan); 198 if (!uploadChan) { 199 return; 200 } 201 202 constexpr auto uploadData = "PING"_ns; 203 204 nsCOMPtr<nsIInputStream> uploadStream; 205 rv = NS_NewCStringInputStream(getter_AddRefs(uploadStream), uploadData); 206 if (NS_WARN_IF(NS_FAILED(rv))) { 207 return; 208 } 209 210 uploadChan->ExplicitSetUploadStream(uploadStream, "text/ping"_ns, 211 uploadData.Length(), "POST"_ns, false); 212 213 // The channel needs to have a loadgroup associated with it, so that we can 214 // cancel the channel and any redirected channels it may create. 215 nsCOMPtr<nsILoadGroup> loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); 216 if (!loadGroup) { 217 return; 218 } 219 nsCOMPtr<nsIInterfaceRequestor> callbacks = do_QueryInterface(info->docShell); 220 loadGroup->SetNotificationCallbacks(callbacks); 221 chan->SetLoadGroup(loadGroup); 222 223 RefPtr<nsPingListener> pingListener = new nsPingListener(); 224 chan->AsyncOpen(pingListener); 225 226 // Even if AsyncOpen failed, we still count this as a successful ping. It's 227 // possible that AsyncOpen may have failed after triggering some background 228 // process that may have written something to the network. 229 info->numPings++; 230 231 // Prevent ping requests from stalling and never being garbage collected... 232 if (NS_FAILED(pingListener->StartTimeout(doc->GetDocGroup()))) { 233 // If we failed to setup the timer, then we should just cancel the channel 234 // because we won't be able to ensure that it goes away in a timely manner. 235 chan->Cancel(NS_ERROR_ABORT); 236 return; 237 } 238 // if the channel openend successfully, then make the pingListener hold 239 // a strong reference to the loadgroup which is released in ::OnStopRequest 240 pingListener->SetLoadGroup(loadGroup); 241 } 242 243 typedef void (*ForEachPingCallback)(void* closure, nsIContent* content, 244 nsIURI* uri, nsIIOService* ios); 245 246 static void ForEachPing(nsIContent* aContent, ForEachPingCallback aCallback, 247 void* aClosure) { 248 // NOTE: Using nsIDOMHTMLAnchorElement::GetPing isn't really worth it here 249 // since we'd still need to parse the resulting string. Instead, we 250 // just parse the raw attribute. It might be nice if the content node 251 // implemented an interface that exposed an enumeration of nsIURIs. 252 253 // Make sure we are dealing with either an <A> or <AREA> element in the HTML 254 // or XHTML namespace, or an <a> element in the SVG namespace. 255 if (!aContent->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area) && 256 !aContent->IsSVGElement(nsGkAtoms::a)) { 257 return; 258 } 259 260 nsAutoString value; 261 aContent->AsElement()->GetAttr(nsGkAtoms::ping, value); 262 if (value.IsEmpty()) { 263 return; 264 } 265 266 nsCOMPtr<nsIIOService> ios = do_GetIOService(); 267 if (!ios) { 268 return; 269 } 270 271 Document* doc = aContent->OwnerDoc(); 272 nsAutoCString charset; 273 doc->GetDocumentCharacterSet()->Name(charset); 274 275 nsWhitespaceTokenizer tokenizer(value); 276 277 while (tokenizer.hasMoreTokens()) { 278 nsCOMPtr<nsIURI> uri; 279 NS_NewURI(getter_AddRefs(uri), tokenizer.nextToken(), charset.get(), 280 aContent->GetBaseURI()); 281 // if we can't generate a valid URI, then there is nothing to do 282 if (!uri) { 283 continue; 284 } 285 // Explicitly not allow loading data: URIs 286 if (!uri->SchemeIs("data")) { 287 aCallback(aClosure, aContent, uri, ios); 288 } 289 } 290 } 291 292 // Spec: http://whatwg.org/specs/web-apps/current-work/#ping 293 /*static*/ void nsPingListener::DispatchPings(nsIDocShell* aDocShell, 294 nsIContent* aContent, 295 nsIURI* aTarget, 296 nsIReferrerInfo* aReferrerInfo) { 297 SendPingInfo info; 298 299 if (!PingsEnabled(&info.maxPings, &info.requireSameHost)) { 300 return; 301 } 302 if (info.maxPings == 0) { 303 return; 304 } 305 306 info.numPings = 0; 307 info.target = aTarget; 308 info.referrerInfo = aReferrerInfo; 309 info.docShell = aDocShell; 310 311 ForEachPing(aContent, SendPing, &info); 312 } 313 314 nsPingListener::~nsPingListener() { 315 if (mTimer) { 316 mTimer->Cancel(); 317 mTimer = nullptr; 318 } 319 } 320 321 nsresult nsPingListener::StartTimeout(DocGroup* aDocGroup) { 322 NS_ENSURE_ARG(aDocGroup); 323 324 return NS_NewTimerWithFuncCallback( 325 getter_AddRefs(mTimer), OnPingTimeout, mLoadGroup, PING_TIMEOUT, 326 nsITimer::TYPE_ONE_SHOT, "nsPingListener::StartTimeout"_ns, 327 GetMainThreadSerialEventTarget()); 328 } 329 330 NS_IMETHODIMP 331 nsPingListener::OnStartRequest(nsIRequest* aRequest) { return NS_OK; } 332 333 NS_IMETHODIMP 334 nsPingListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream, 335 uint64_t aOffset, uint32_t aCount) { 336 uint32_t result; 337 return aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &result); 338 } 339 340 NS_IMETHODIMP 341 nsPingListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { 342 mLoadGroup = nullptr; 343 344 if (mTimer) { 345 mTimer->Cancel(); 346 mTimer = nullptr; 347 } 348 349 return NS_OK; 350 }