tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }