Source code

Revision control

Other Tools

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 "nsIHttpChannelInternal.h"
16
#include "nsIInputStream.h"
17
#include "nsIProtocolHandler.h"
18
#include "nsIUploadChannel2.h"
19
20
#include "nsNetUtil.h"
21
#include "nsStreamUtils.h"
22
#include "nsStringStream.h"
23
#include "nsWhitespaceTokenizer.h"
24
25
using namespace mozilla;
26
using namespace mozilla::dom;
27
28
NS_IMPL_ISUPPORTS(nsPingListener, nsIStreamListener, nsIRequestObserver)
29
30
//*****************************************************************************
31
// <a ping> support
32
//*****************************************************************************
33
34
#define PREF_PINGS_ENABLED "browser.send_pings"
35
#define PREF_PINGS_MAX_PER_LINK "browser.send_pings.max_per_link"
36
#define PREF_PINGS_REQUIRE_SAME_HOST "browser.send_pings.require_same_host"
37
38
// Check prefs to see if pings are enabled and if so what restrictions might
39
// be applied.
40
//
41
// @param maxPerLink
42
// This parameter returns the number of pings that are allowed per link click
43
//
44
// @param requireSameHost
45
// This parameter returns true if pings are restricted to the same host as
46
// the document in which the click occurs. If the same host restriction is
47
// imposed, then we still allow for pings to cross over to different
48
// protocols and ports for flexibility and because it is not possible to send
49
// a ping via FTP.
50
//
51
// @returns
52
// true if pings are enabled and false otherwise.
53
//
54
static bool PingsEnabled(int32_t* aMaxPerLink, bool* aRequireSameHost) {
55
bool allow = Preferences::GetBool(PREF_PINGS_ENABLED, false);
56
57
*aMaxPerLink = 1;
58
*aRequireSameHost = true;
59
60
if (allow) {
61
Preferences::GetInt(PREF_PINGS_MAX_PER_LINK, aMaxPerLink);
62
Preferences::GetBool(PREF_PINGS_REQUIRE_SAME_HOST, aRequireSameHost);
63
}
64
65
return allow;
66
}
67
68
// We wait this many milliseconds before killing the ping channel...
69
#define PING_TIMEOUT 10000
70
71
static void OnPingTimeout(nsITimer* aTimer, void* aClosure) {
72
nsILoadGroup* loadGroup = static_cast<nsILoadGroup*>(aClosure);
73
if (loadGroup) {
74
loadGroup->Cancel(NS_ERROR_ABORT);
75
}
76
}
77
78
struct MOZ_STACK_CLASS SendPingInfo {
79
int32_t numPings;
80
int32_t maxPings;
81
bool requireSameHost;
82
nsIURI* target;
83
nsIReferrerInfo* referrerInfo;
84
nsIDocShell* docShell;
85
};
86
87
static void SendPing(void* aClosure, nsIContent* aContent, nsIURI* aURI,
88
nsIIOService* aIOService) {
89
SendPingInfo* info = static_cast<SendPingInfo*>(aClosure);
90
if (info->maxPings > -1 && info->numPings >= info->maxPings) {
91
return;
92
}
93
94
Document* doc = aContent->OwnerDoc();
95
96
nsCOMPtr<nsIChannel> chan;
97
NS_NewChannel(getter_AddRefs(chan), aURI, doc,
98
info->requireSameHost
99
? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED
100
: nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
101
nsIContentPolicy::TYPE_PING,
102
nullptr, // PerformanceStorage
103
nullptr, // aLoadGroup
104
nullptr, // aCallbacks
105
nsIRequest::LOAD_NORMAL, // aLoadFlags,
106
aIOService);
107
108
if (!chan) {
109
return;
110
}
111
112
// Don't bother caching the result of this URI load, but do not exempt
113
// it from Safe Browsing.
114
chan->SetLoadFlags(nsIRequest::INHIBIT_CACHING);
115
116
nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan);
117
if (!httpChan) {
118
return;
119
}
120
121
// This is needed in order for 3rd-party cookie blocking to work.
122
nsCOMPtr<nsIHttpChannelInternal> httpInternal = do_QueryInterface(httpChan);
123
nsresult rv;
124
if (httpInternal) {
125
rv = httpInternal->SetDocumentURI(doc->GetDocumentURI());
126
MOZ_ASSERT(NS_SUCCEEDED(rv));
127
}
128
129
rv = httpChan->SetRequestMethod(NS_LITERAL_CSTRING("POST"));
130
MOZ_ASSERT(NS_SUCCEEDED(rv));
131
132
// Remove extraneous request headers (to reduce request size)
133
rv = httpChan->SetRequestHeader(NS_LITERAL_CSTRING("accept"), EmptyCString(),
134
false);
135
MOZ_ASSERT(NS_SUCCEEDED(rv));
136
rv = httpChan->SetRequestHeader(NS_LITERAL_CSTRING("accept-language"),
137
EmptyCString(), false);
138
MOZ_ASSERT(NS_SUCCEEDED(rv));
139
rv = httpChan->SetRequestHeader(NS_LITERAL_CSTRING("accept-encoding"),
140
EmptyCString(), false);
141
MOZ_ASSERT(NS_SUCCEEDED(rv));
142
143
// Always send a Ping-To header.
144
nsAutoCString pingTo;
145
if (NS_SUCCEEDED(info->target->GetSpec(pingTo))) {
146
rv = httpChan->SetRequestHeader(NS_LITERAL_CSTRING("Ping-To"), pingTo,
147
false);
148
MOZ_ASSERT(NS_SUCCEEDED(rv));
149
}
150
151
nsCOMPtr<nsIScriptSecurityManager> sm =
152
do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
153
154
if (sm && info->referrerInfo) {
155
nsCOMPtr<nsIURI> referrer = info->referrerInfo->GetOriginalReferrer();
156
bool referrerIsSecure = false;
157
uint32_t flags = nsIProtocolHandler::URI_IS_POTENTIALLY_TRUSTWORTHY;
158
if (referrer) {
159
rv = NS_URIChainHasFlags(referrer, flags, &referrerIsSecure);
160
}
161
162
// Default to sending less data if NS_URIChainHasFlags() fails.
163
referrerIsSecure = NS_FAILED(rv) || referrerIsSecure;
164
165
bool isPrivateWin = false;
166
if (doc) {
167
isPrivateWin =
168
doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId > 0;
169
}
170
171
bool sameOrigin = NS_SUCCEEDED(
172
sm->CheckSameOriginURI(referrer, aURI, false, isPrivateWin));
173
174
// If both the address of the document containing the hyperlink being
175
// audited and "ping URL" have the same origin or the document containing
176
// the hyperlink being audited was not retrieved over an encrypted
177
// connection, send a Ping-From header.
178
if (sameOrigin || !referrerIsSecure) {
179
nsAutoCString pingFrom;
180
if (NS_SUCCEEDED(referrer->GetSpec(pingFrom))) {
181
rv = httpChan->SetRequestHeader(NS_LITERAL_CSTRING("Ping-From"),
182
pingFrom, false);
183
MOZ_ASSERT(NS_SUCCEEDED(rv));
184
}
185
}
186
187
// If the document containing the hyperlink being audited was not retrieved
188
// over an encrypted connection and its address does not have the same
189
// origin as "ping URL", send a referrer.
190
if (!sameOrigin && !referrerIsSecure && info->referrerInfo) {
191
rv = httpChan->SetReferrerInfo(info->referrerInfo);
192
MOZ_ASSERT(NS_SUCCEEDED(rv));
193
}
194
}
195
196
nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(httpChan);
197
if (!uploadChan) {
198
return;
199
}
200
201
NS_NAMED_LITERAL_CSTRING(uploadData, "PING");
202
203
nsCOMPtr<nsIInputStream> uploadStream;
204
rv = NS_NewCStringInputStream(getter_AddRefs(uploadStream), uploadData);
205
if (NS_WARN_IF(NS_FAILED(rv))) {
206
return;
207
}
208
209
uploadChan->ExplicitSetUploadStream(
210
uploadStream, NS_LITERAL_CSTRING("text/ping"), uploadData.Length(),
211
NS_LITERAL_CSTRING("POST"), 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.
255
if (!aContent->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area)) {
256
return;
257
}
258
259
nsAutoString value;
260
aContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::ping, value);
261
if (value.IsEmpty()) {
262
return;
263
}
264
265
nsCOMPtr<nsIIOService> ios = do_GetIOService();
266
if (!ios) {
267
return;
268
}
269
270
Document* doc = aContent->OwnerDoc();
271
nsAutoCString charset;
272
doc->GetDocumentCharacterSet()->Name(charset);
273
274
nsWhitespaceTokenizer tokenizer(value);
275
276
while (tokenizer.hasMoreTokens()) {
277
nsCOMPtr<nsIURI> uri;
278
NS_NewURI(getter_AddRefs(uri), tokenizer.nextToken(), charset.get(),
279
aContent->GetBaseURI());
280
// if we can't generate a valid URI, then there is nothing to do
281
if (!uri) {
282
continue;
283
}
284
// Explicitly not allow loading data: URIs
285
if (!net::SchemeIsData(uri)) {
286
aCallback(aClosure, aContent, uri, ios);
287
}
288
}
289
}
290
292
/*static*/ void nsPingListener::DispatchPings(nsIDocShell* aDocShell,
293
nsIContent* aContent,
294
nsIURI* aTarget,
295
nsIReferrerInfo* aReferrerInfo) {
296
SendPingInfo info;
297
298
if (!PingsEnabled(&info.maxPings, &info.requireSameHost)) {
299
return;
300
}
301
if (info.maxPings == 0) {
302
return;
303
}
304
305
info.numPings = 0;
306
info.target = aTarget;
307
info.referrerInfo = aReferrerInfo;
308
info.docShell = aDocShell;
309
310
ForEachPing(aContent, SendPing, &info);
311
}
312
313
nsPingListener::~nsPingListener() {
314
if (mTimer) {
315
mTimer->Cancel();
316
mTimer = nullptr;
317
}
318
}
319
320
nsresult nsPingListener::StartTimeout(DocGroup* aDocGroup) {
321
NS_ENSURE_ARG(aDocGroup);
322
323
return NS_NewTimerWithFuncCallback(
324
getter_AddRefs(mTimer), OnPingTimeout, mLoadGroup, PING_TIMEOUT,
325
nsITimer::TYPE_ONE_SHOT, "nsPingListener::StartTimeout",
326
aDocGroup->EventTargetFor(TaskCategory::Network));
327
}
328
329
NS_IMETHODIMP
330
nsPingListener::OnStartRequest(nsIRequest* aRequest) { return NS_OK; }
331
332
NS_IMETHODIMP
333
nsPingListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
334
uint64_t aOffset, uint32_t aCount) {
335
uint32_t result;
336
return aStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &result);
337
}
338
339
NS_IMETHODIMP
340
nsPingListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
341
mLoadGroup = nullptr;
342
343
if (mTimer) {
344
mTimer->Cancel();
345
mTimer = nullptr;
346
}
347
348
return NS_OK;
349
}