1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set sw=2 ts=8 et 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 "mozilla/Attributes.h"
8 #include "mozilla/ClearOnShutdown.h"
9 #include "mozilla/DebugOnly.h"
10 #include "mozilla/Likely.h"
11 #include "mozilla/Printf.h"
12 #include "mozilla/Unused.h"
13
14 #include "mozilla/net/CookieServiceChild.h"
15 #include "mozilla/net/NeckoCommon.h"
16
17 #include "nsCookieService.h"
18 #include "nsContentUtils.h"
19 #include "nsIServiceManager.h"
20 #include "nsIScriptSecurityManager.h"
21
22 #include "nsIIOService.h"
23 #include "nsIPermissionManager.h"
24 #include "nsIProtocolHandler.h"
25 #include "nsIPrefBranch.h"
26 #include "nsIPrefService.h"
27 #include "nsIScriptError.h"
28 #include "nsICookiePermission.h"
29 #include "nsIURI.h"
30 #include "nsIURL.h"
31 #include "nsIChannel.h"
32 #include "nsIFile.h"
33 #include "nsIObserverService.h"
34 #include "nsILineInputStream.h"
35 #include "nsIEffectiveTLDService.h"
36 #include "nsIIDNService.h"
37 #include "nsIThread.h"
38 #include "mozIThirdPartyUtil.h"
39
40 #include "nsTArray.h"
41 #include "nsCOMArray.h"
42 #include "nsIMutableArray.h"
43 #include "nsArrayEnumerator.h"
44 #include "nsEnumeratorUtils.h"
45 #include "nsAutoPtr.h"
46 #include "nsReadableUtils.h"
47 #include "nsCRT.h"
48 #include "prprf.h"
49 #include "nsNetUtil.h"
50 #include "nsNetCID.h"
51 #include "nsISimpleEnumerator.h"
52 #include "nsIInputStream.h"
53 #include "nsAppDirectoryServiceDefs.h"
54 #include "nsNetCID.h"
55 #include "mozilla/storage.h"
56 #include "mozilla/AutoRestore.h"
57 #include "mozilla/FileUtils.h"
58 #include "mozilla/ScopeExit.h"
59 #include "mozilla/Telemetry.h"
60 #include "nsIConsoleService.h"
61 #include "nsVariant.h"
62
63 using namespace mozilla;
64 using namespace mozilla::net;
65
66 // Create key from baseDomain that will access the default cookie namespace.
67 // TODO: When we figure out what the API will look like for nsICookieManager{2}
68 // on content processes (see bug 777620), change to use the appropriate app
69 // namespace. For now those IDLs aren't supported on child processes.
70 #define DEFAULT_APP_KEY(baseDomain) nsCookieKey(baseDomain, OriginAttributes())
71
72 /******************************************************************************
73 * nsCookieService impl:
74 * useful types & constants
75 ******************************************************************************/
76
77 static StaticRefPtr<nsCookieService> gCookieService;
78 bool nsCookieService::sSameSiteEnabled = false;
79
80 // XXX_hack. See bug 178993.
81 // This is a hack to hide HttpOnly cookies from older browsers
82 #define HTTP_ONLY_PREFIX "#HttpOnly_"
83
84 #define COOKIES_FILE "cookies.sqlite"
85 #define COOKIES_SCHEMA_VERSION 9
86
87 // parameter indexes; see |Read|
88 #define IDX_NAME 0
89 #define IDX_VALUE 1
90 #define IDX_HOST 2
91 #define IDX_PATH 3
92 #define IDX_EXPIRY 4
93 #define IDX_LAST_ACCESSED 5
94 #define IDX_CREATION_TIME 6
95 #define IDX_SECURE 7
96 #define IDX_HTTPONLY 8
97 #define IDX_BASE_DOMAIN 9
98 #define IDX_ORIGIN_ATTRIBUTES 10
99 #define IDX_SAME_SITE 11
100
101 #define TOPIC_CLEAR_ORIGIN_DATA "clear-origin-attributes-data"
102
103 static const int64_t kCookiePurgeAge =
104 int64_t(30 * 24 * 60 * 60) * PR_USEC_PER_SEC; // 30 days in microseconds
105
106 #define OLD_COOKIE_FILE_NAME "cookies.txt"
107
108 #undef LIMIT
109 #define LIMIT(x, low, high, default) \
110 ((x) >= (low) && (x) <= (high) ? (x) : (default))
111
112 #undef ADD_TEN_PERCENT
113 #define ADD_TEN_PERCENT(i) static_cast<uint32_t>((i) + (i) / 10)
114
115 // default limits for the cookie list. these can be tuned by the
116 // network.cookie.maxNumber and network.cookie.maxPerHost prefs respectively.
117 static const uint32_t kMaxNumberOfCookies = 3000;
118 static const uint32_t kMaxCookiesPerHost = 150;
119 static const uint32_t kMaxBytesPerCookie = 4096;
120 static const uint32_t kMaxBytesPerPath = 1024;
121
122 // pref string constants
123 static const char kPrefCookieBehavior[] = "network.cookie.cookieBehavior";
124 static const char kPrefMaxNumberOfCookies[] = "network.cookie.maxNumber";
125 static const char kPrefMaxCookiesPerHost[] = "network.cookie.maxPerHost";
126 static const char kPrefCookiePurgeAge[] = "network.cookie.purgeAge";
127 static const char kPrefThirdPartySession[] =
128 "network.cookie.thirdparty.sessionOnly";
129 static const char kPrefThirdPartyNonsecureSession[] =
130 "network.cookie.thirdparty.nonsecureSessionOnly";
131 static const char kCookieLeaveSecurityAlone[] =
132 "network.cookie.leave-secure-alone";
133
134 // For telemetry COOKIE_LEAVE_SECURE_ALONE
135 #define BLOCKED_SECURE_SET_FROM_HTTP 0
136 #define BLOCKED_DOWNGRADE_SECURE_INEXACT 1
137 #define DOWNGRADE_SECURE_FROM_SECURE_INEXACT 2
138 #define EVICTED_NEWER_INSECURE 3
139 #define EVICTED_OLDEST_COOKIE 4
140 #define EVICTED_PREFERRED_COOKIE 5
141 #define EVICTING_SECURE_BLOCKED 6
142 #define BLOCKED_DOWNGRADE_SECURE_EXACT 7
143 #define DOWNGRADE_SECURE_FROM_SECURE_EXACT 8
144
145 static void bindCookieParameters(mozIStorageBindingParamsArray *aParamsArray,
146 const nsCookieKey &aKey,
147 const nsCookie *aCookie);
148
149 // stores the nsCookieEntry entryclass and an index into the cookie array
150 // within that entryclass, for purposes of storing an iteration state that
151 // points to a certain cookie.
152 struct nsListIter {
153 // default (non-initializing) constructor.
154 nsListIter() = default;
155
156 // explicit constructor to a given iterator state with entryclass 'aEntry'
157 // and index 'aIndex'.
nsListIternsListIter158 explicit nsListIter(nsCookieEntry *aEntry, nsCookieEntry::IndexType aIndex)
159 : entry(aEntry), index(aIndex) {}
160
161 // get the nsCookie * the iterator currently points to.
CookiensListIter162 nsCookie *Cookie() const { return entry->GetCookies()[index]; }
163
164 nsCookieEntry *entry;
165 nsCookieEntry::IndexType index;
166 };
167
168 /******************************************************************************
169 * Cookie logging handlers
170 * used for logging in nsCookieService
171 ******************************************************************************/
172
173 // logging handlers
174 #ifdef MOZ_LOGGING
175 // in order to do logging, the following environment variables need to be set:
176 //
177 // set MOZ_LOG=cookie:3 -- shows rejected cookies
178 // set MOZ_LOG=cookie:4 -- shows accepted and rejected cookies
179 // set MOZ_LOG_FILE=cookie.log
180 //
181 #include "mozilla/Logging.h"
182 #endif
183
184 // define logging macros for convenience
185 #define SET_COOKIE true
186 #define GET_COOKIE false
187
188 static LazyLogModule gCookieLog("cookie");
189
190 #define COOKIE_LOGFAILURE(a, b, c, d) LogFailure(a, b, c, d)
191 #define COOKIE_LOGSUCCESS(a, b, c, d, e) LogSuccess(a, b, c, d, e)
192
193 #define COOKIE_LOGEVICTED(a, details) \
194 PR_BEGIN_MACRO \
195 if (MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) LogEvicted(a, details); \
196 PR_END_MACRO
197
198 #define COOKIE_LOGSTRING(lvl, fmt) \
199 PR_BEGIN_MACRO \
200 MOZ_LOG(gCookieLog, lvl, fmt); \
201 MOZ_LOG(gCookieLog, lvl, ("\n")); \
202 PR_END_MACRO
203
LogFailure(bool aSetCookie,nsIURI * aHostURI,const char * aCookieString,const char * aReason)204 static void LogFailure(bool aSetCookie, nsIURI *aHostURI,
205 const char *aCookieString, const char *aReason) {
206 // if logging isn't enabled, return now to save cycles
207 if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) return;
208
209 nsAutoCString spec;
210 if (aHostURI) aHostURI->GetAsciiSpec(spec);
211
212 MOZ_LOG(gCookieLog, LogLevel::Warning,
213 ("===== %s =====\n",
214 aSetCookie ? "COOKIE NOT ACCEPTED" : "COOKIE NOT SENT"));
215 MOZ_LOG(gCookieLog, LogLevel::Warning, ("request URL: %s\n", spec.get()));
216 if (aSetCookie)
217 MOZ_LOG(gCookieLog, LogLevel::Warning,
218 ("cookie string: %s\n", aCookieString));
219
220 PRExplodedTime explodedTime;
221 PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime);
222 char timeString[40];
223 PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
224
225 MOZ_LOG(gCookieLog, LogLevel::Warning, ("current time: %s", timeString));
226 MOZ_LOG(gCookieLog, LogLevel::Warning, ("rejected because %s\n", aReason));
227 MOZ_LOG(gCookieLog, LogLevel::Warning, ("\n"));
228 }
229
LogCookie(nsCookie * aCookie)230 static void LogCookie(nsCookie *aCookie) {
231 PRExplodedTime explodedTime;
232 PR_ExplodeTime(PR_Now(), PR_GMTParameters, &explodedTime);
233 char timeString[40];
234 PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
235
236 MOZ_LOG(gCookieLog, LogLevel::Debug, ("current time: %s", timeString));
237
238 if (aCookie) {
239 MOZ_LOG(gCookieLog, LogLevel::Debug, ("----------------\n"));
240 MOZ_LOG(gCookieLog, LogLevel::Debug, ("name: %s\n", aCookie->Name().get()));
241 MOZ_LOG(gCookieLog, LogLevel::Debug,
242 ("value: %s\n", aCookie->Value().get()));
243 MOZ_LOG(gCookieLog, LogLevel::Debug,
244 ("%s: %s\n", aCookie->IsDomain() ? "domain" : "host",
245 aCookie->Host().get()));
246 MOZ_LOG(gCookieLog, LogLevel::Debug, ("path: %s\n", aCookie->Path().get()));
247
248 PR_ExplodeTime(aCookie->Expiry() * int64_t(PR_USEC_PER_SEC),
249 PR_GMTParameters, &explodedTime);
250 PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
251 MOZ_LOG(gCookieLog, LogLevel::Debug,
252 ("expires: %s%s", timeString,
253 aCookie->IsSession() ? " (at end of session)" : ""));
254
255 PR_ExplodeTime(aCookie->CreationTime(), PR_GMTParameters, &explodedTime);
256 PR_FormatTimeUSEnglish(timeString, 40, "%c GMT", &explodedTime);
257 MOZ_LOG(gCookieLog, LogLevel::Debug, ("created: %s", timeString));
258
259 MOZ_LOG(gCookieLog, LogLevel::Debug,
260 ("is secure: %s\n", aCookie->IsSecure() ? "true" : "false"));
261 MOZ_LOG(gCookieLog, LogLevel::Debug,
262 ("is httpOnly: %s\n", aCookie->IsHttpOnly() ? "true" : "false"));
263
264 nsAutoCString suffix;
265 aCookie->OriginAttributesRef().CreateSuffix(suffix);
266 MOZ_LOG(gCookieLog, LogLevel::Debug,
267 ("origin attributes: %s\n",
268 suffix.IsEmpty() ? "{empty}" : suffix.get()));
269 }
270 }
271
LogSuccess(bool aSetCookie,nsIURI * aHostURI,const char * aCookieString,nsCookie * aCookie,bool aReplacing)272 static void LogSuccess(bool aSetCookie, nsIURI *aHostURI,
273 const char *aCookieString, nsCookie *aCookie,
274 bool aReplacing) {
275 // if logging isn't enabled, return now to save cycles
276 if (!MOZ_LOG_TEST(gCookieLog, LogLevel::Debug)) {
277 return;
278 }
279
280 nsAutoCString spec;
281 if (aHostURI) aHostURI->GetAsciiSpec(spec);
282
283 MOZ_LOG(gCookieLog, LogLevel::Debug,
284 ("===== %s =====\n", aSetCookie ? "COOKIE ACCEPTED" : "COOKIE SENT"));
285 MOZ_LOG(gCookieLog, LogLevel::Debug, ("request URL: %s\n", spec.get()));
286 MOZ_LOG(gCookieLog, LogLevel::Debug, ("cookie string: %s\n", aCookieString));
287 if (aSetCookie)
288 MOZ_LOG(gCookieLog, LogLevel::Debug,
289 ("replaces existing cookie: %s\n", aReplacing ? "true" : "false"));
290
291 LogCookie(aCookie);
292
293 MOZ_LOG(gCookieLog, LogLevel::Debug, ("\n"));
294 }
295
LogEvicted(nsCookie * aCookie,const char * details)296 static void LogEvicted(nsCookie *aCookie, const char *details) {
297 MOZ_LOG(gCookieLog, LogLevel::Debug, ("===== COOKIE EVICTED =====\n"));
298 MOZ_LOG(gCookieLog, LogLevel::Debug, ("%s\n", details));
299
300 LogCookie(aCookie);
301
302 MOZ_LOG(gCookieLog, LogLevel::Debug, ("\n"));
303 }
304
305 // inline wrappers to make passing in nsCStrings easier
LogFailure(bool aSetCookie,nsIURI * aHostURI,const nsCString & aCookieString,const char * aReason)306 static inline void LogFailure(bool aSetCookie, nsIURI *aHostURI,
307 const nsCString &aCookieString,
308 const char *aReason) {
309 LogFailure(aSetCookie, aHostURI, aCookieString.get(), aReason);
310 }
311
LogSuccess(bool aSetCookie,nsIURI * aHostURI,const nsCString & aCookieString,nsCookie * aCookie,bool aReplacing)312 static inline void LogSuccess(bool aSetCookie, nsIURI *aHostURI,
313 const nsCString &aCookieString, nsCookie *aCookie,
314 bool aReplacing) {
315 LogSuccess(aSetCookie, aHostURI, aCookieString.get(), aCookie, aReplacing);
316 }
317
318 #ifdef DEBUG
319 #define NS_ASSERT_SUCCESS(res) \
320 PR_BEGIN_MACRO \
321 nsresult __rv = res; /* Do not evaluate |res| more than once! */ \
322 if (NS_FAILED(__rv)) { \
323 SmprintfPointer msg = mozilla::Smprintf( \
324 "NS_ASSERT_SUCCESS(%s) failed with result 0x%" PRIX32, #res, \
325 static_cast<uint32_t>(__rv)); \
326 NS_ASSERTION(NS_SUCCEEDED(__rv), msg.get()); \
327 } \
328 PR_END_MACRO
329 #else
330 #define NS_ASSERT_SUCCESS(res) PR_BEGIN_MACRO /* nothing */ PR_END_MACRO
331 #endif
332
333 /******************************************************************************
334 * DBListenerErrorHandler impl:
335 * Parent class for our async storage listeners that handles the logging of
336 * errors.
337 ******************************************************************************/
338 class DBListenerErrorHandler : public mozIStorageStatementCallback {
339 protected:
DBListenerErrorHandler(DBState * dbState)340 explicit DBListenerErrorHandler(DBState *dbState) : mDBState(dbState) {}
341 RefPtr<DBState> mDBState;
342 virtual const char *GetOpType() = 0;
343
344 public:
HandleError(mozIStorageError * aError)345 NS_IMETHOD HandleError(mozIStorageError *aError) override {
346 if (MOZ_LOG_TEST(gCookieLog, LogLevel::Warning)) {
347 int32_t result = -1;
348 aError->GetResult(&result);
349
350 nsAutoCString message;
351 aError->GetMessage(message);
352 COOKIE_LOGSTRING(
353 LogLevel::Warning,
354 ("DBListenerErrorHandler::HandleError(): Error %d occurred while "
355 "performing operation '%s' with message '%s'; rebuilding database.",
356 result, GetOpType(), message.get()));
357 }
358
359 // Rebuild the database.
360 gCookieService->HandleCorruptDB(mDBState);
361
362 return NS_OK;
363 }
364 };
365
366 /******************************************************************************
367 * InsertCookieDBListener impl:
368 * mozIStorageStatementCallback used to track asynchronous insertion operations.
369 ******************************************************************************/
370 class InsertCookieDBListener final : public DBListenerErrorHandler {
371 private:
GetOpType()372 const char *GetOpType() override { return "INSERT"; }
373
374 ~InsertCookieDBListener() = default;
375
376 public:
377 NS_DECL_ISUPPORTS
378
InsertCookieDBListener(DBState * dbState)379 explicit InsertCookieDBListener(DBState *dbState)
380 : DBListenerErrorHandler(dbState) {}
HandleResult(mozIStorageResultSet *)381 NS_IMETHOD HandleResult(mozIStorageResultSet *) override {
382 NS_NOTREACHED("Unexpected call to InsertCookieDBListener::HandleResult");
383 return NS_OK;
384 }
HandleCompletion(uint16_t aReason)385 NS_IMETHOD HandleCompletion(uint16_t aReason) override {
386 // If we were rebuilding the db and we succeeded, make our corruptFlag say
387 // so.
388 if (mDBState->corruptFlag == DBState::REBUILDING &&
389 aReason == mozIStorageStatementCallback::REASON_FINISHED) {
390 COOKIE_LOGSTRING(
391 LogLevel::Debug,
392 ("InsertCookieDBListener::HandleCompletion(): rebuild complete"));
393 mDBState->corruptFlag = DBState::OK;
394 }
395 return NS_OK;
396 }
397 };
398
399 NS_IMPL_ISUPPORTS(InsertCookieDBListener, mozIStorageStatementCallback)
400
401 /******************************************************************************
402 * UpdateCookieDBListener impl:
403 * mozIStorageStatementCallback used to track asynchronous update operations.
404 ******************************************************************************/
405 class UpdateCookieDBListener final : public DBListenerErrorHandler {
406 private:
GetOpType()407 const char *GetOpType() override { return "UPDATE"; }
408
409 ~UpdateCookieDBListener() = default;
410
411 public:
412 NS_DECL_ISUPPORTS
413
UpdateCookieDBListener(DBState * dbState)414 explicit UpdateCookieDBListener(DBState *dbState)
415 : DBListenerErrorHandler(dbState) {}
HandleResult(mozIStorageResultSet *)416 NS_IMETHOD HandleResult(mozIStorageResultSet *) override {
417 NS_NOTREACHED("Unexpected call to UpdateCookieDBListener::HandleResult");
418 return NS_OK;
419 }
HandleCompletion(uint16_t aReason)420 NS_IMETHOD HandleCompletion(uint16_t aReason) override { return NS_OK; }
421 };
422
423 NS_IMPL_ISUPPORTS(UpdateCookieDBListener, mozIStorageStatementCallback)
424
425 /******************************************************************************
426 * RemoveCookieDBListener impl:
427 * mozIStorageStatementCallback used to track asynchronous removal operations.
428 ******************************************************************************/
429 class RemoveCookieDBListener final : public DBListenerErrorHandler {
430 private:
GetOpType()431 const char *GetOpType() override { return "REMOVE"; }
432
433 ~RemoveCookieDBListener() = default;
434
435 public:
436 NS_DECL_ISUPPORTS
437
RemoveCookieDBListener(DBState * dbState)438 explicit RemoveCookieDBListener(DBState *dbState)
439 : DBListenerErrorHandler(dbState) {}
HandleResult(mozIStorageResultSet *)440 NS_IMETHOD HandleResult(mozIStorageResultSet *) override {
441 NS_NOTREACHED("Unexpected call to RemoveCookieDBListener::HandleResult");
442 return NS_OK;
443 }
HandleCompletion(uint16_t aReason)444 NS_IMETHOD HandleCompletion(uint16_t aReason) override { return NS_OK; }
445 };
446
447 NS_IMPL_ISUPPORTS(RemoveCookieDBListener, mozIStorageStatementCallback)
448
449 /******************************************************************************
450 * CloseCookieDBListener imp:
451 * Static mozIStorageCompletionCallback used to notify when the database is
452 * successfully closed.
453 ******************************************************************************/
454 class CloseCookieDBListener final : public mozIStorageCompletionCallback {
455 ~CloseCookieDBListener() = default;
456
457 public:
CloseCookieDBListener(DBState * dbState)458 explicit CloseCookieDBListener(DBState *dbState) : mDBState(dbState) {}
459 RefPtr<DBState> mDBState;
460 NS_DECL_ISUPPORTS
461
Complete(nsresult,nsISupports *)462 NS_IMETHOD Complete(nsresult, nsISupports *) override {
463 gCookieService->HandleDBClosed(mDBState);
464 return NS_OK;
465 }
466 };
467
468 NS_IMPL_ISUPPORTS(CloseCookieDBListener, mozIStorageCompletionCallback)
469
470 namespace {
471
472 class AppClearDataObserver final : public nsIObserver {
473 ~AppClearDataObserver() = default;
474
475 public:
476 NS_DECL_ISUPPORTS
477
478 // nsIObserver implementation.
479 NS_IMETHOD
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)480 Observe(nsISupports *aSubject, const char *aTopic,
481 const char16_t *aData) override {
482 MOZ_ASSERT(!nsCRT::strcmp(aTopic, TOPIC_CLEAR_ORIGIN_DATA));
483
484 MOZ_ASSERT(XRE_IsParentProcess());
485
486 nsCOMPtr<nsICookieManager> cookieManager =
487 do_GetService(NS_COOKIEMANAGER_CONTRACTID);
488 MOZ_ASSERT(cookieManager);
489
490 return cookieManager->RemoveCookiesWithOriginAttributes(
491 nsDependentString(aData), EmptyCString());
492 }
493 };
494
495 NS_IMPL_ISUPPORTS(AppClearDataObserver, nsIObserver)
496
497 } // namespace
498
SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const499 size_t nsCookieEntry::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
500 size_t amount = nsCookieKey::SizeOfExcludingThis(aMallocSizeOf);
501
502 amount += mCookies.ShallowSizeOfExcludingThis(aMallocSizeOf);
503 for (uint32_t i = 0; i < mCookies.Length(); ++i) {
504 amount += mCookies[i]->SizeOfIncludingThis(aMallocSizeOf);
505 }
506
507 return amount;
508 }
509
SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const510 size_t DBState::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
511 size_t amount = 0;
512
513 amount += aMallocSizeOf(this);
514 amount += hostTable.SizeOfExcludingThis(aMallocSizeOf);
515
516 return amount;
517 }
518
519 /******************************************************************************
520 * nsCookieService impl:
521 * singleton instance ctor/dtor methods
522 ******************************************************************************/
523
GetXPCOMSingleton()524 already_AddRefed<nsICookieService> nsCookieService::GetXPCOMSingleton() {
525 if (IsNeckoChild()) return CookieServiceChild::GetSingleton();
526
527 return GetSingleton();
528 }
529
GetSingleton()530 already_AddRefed<nsCookieService> nsCookieService::GetSingleton() {
531 NS_ASSERTION(!IsNeckoChild(), "not a parent process");
532
533 if (gCookieService) {
534 return do_AddRef(gCookieService);
535 }
536
537 // Create a new singleton nsCookieService.
538 // We AddRef only once since XPCOM has rules about the ordering of module
539 // teardowns - by the time our module destructor is called, it's too late to
540 // Release our members (e.g. nsIObserverService and nsIPrefBranch), since GC
541 // cycles have already been completed and would result in serious leaks.
542 // See bug 209571.
543 gCookieService = new nsCookieService();
544 if (gCookieService) {
545 if (NS_SUCCEEDED(gCookieService->Init())) {
546 ClearOnShutdown(&gCookieService);
547 } else {
548 gCookieService = nullptr;
549 }
550 }
551
552 return do_AddRef(gCookieService);
553 }
554
AppClearDataObserverInit()555 /* static */ void nsCookieService::AppClearDataObserverInit() {
556 nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
557 nsCOMPtr<nsIObserver> obs = new AppClearDataObserver();
558 observerService->AddObserver(obs, TOPIC_CLEAR_ORIGIN_DATA,
559 /* ownsWeak= */ false);
560 }
561
562 /******************************************************************************
563 * nsCookieService impl:
564 * public methods
565 ******************************************************************************/
566
NS_IMPL_ISUPPORTS(nsCookieService,nsICookieService,nsICookieManager,nsIObserver,nsISupportsWeakReference,nsIMemoryReporter)567 NS_IMPL_ISUPPORTS(nsCookieService, nsICookieService, nsICookieManager,
568 nsIObserver, nsISupportsWeakReference, nsIMemoryReporter)
569
570 nsCookieService::nsCookieService()
571 : mDBState(nullptr),
572 mCookieBehavior(nsICookieService::BEHAVIOR_ACCEPT),
573 mThirdPartySession(false),
574 mThirdPartyNonsecureSession(false),
575 mLeaveSecureAlone(true),
576 mMaxNumberOfCookies(kMaxNumberOfCookies),
577 mMaxCookiesPerHost(kMaxCookiesPerHost),
578 mCookiePurgeAge(kCookiePurgeAge),
579 mThread(nullptr),
580 mMonitor("CookieThread"),
581 mInitializedDBStates(false),
582 mInitializedDBConn(false) {}
583
Init()584 nsresult nsCookieService::Init() {
585 nsresult rv;
586 mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv);
587 NS_ENSURE_SUCCESS(rv, rv);
588
589 mIDNService = do_GetService(NS_IDNSERVICE_CONTRACTID, &rv);
590 NS_ENSURE_SUCCESS(rv, rv);
591
592 mThirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID);
593 NS_ENSURE_SUCCESS(rv, rv);
594
595 // init our pref and observer
596 nsCOMPtr<nsIPrefBranch> prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID);
597 if (prefBranch) {
598 prefBranch->AddObserver(kPrefCookieBehavior, this, true);
599 prefBranch->AddObserver(kPrefMaxNumberOfCookies, this, true);
600 prefBranch->AddObserver(kPrefMaxCookiesPerHost, this, true);
601 prefBranch->AddObserver(kPrefCookiePurgeAge, this, true);
602 prefBranch->AddObserver(kPrefThirdPartySession, this, true);
603 prefBranch->AddObserver(kPrefThirdPartyNonsecureSession, this, true);
604 prefBranch->AddObserver(kCookieLeaveSecurityAlone, this, true);
605 PrefChanged(prefBranch);
606 }
607
608 mStorageService = do_GetService("@mozilla.org/storage/service;1", &rv);
609 NS_ENSURE_SUCCESS(rv, rv);
610
611 // Init our default, and possibly private DBStates.
612 InitDBStates();
613
614 RegisterWeakMemoryReporter(this);
615
616 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
617 NS_ENSURE_STATE(os);
618 os->AddObserver(this, "profile-before-change", true);
619 os->AddObserver(this, "profile-do-change", true);
620 os->AddObserver(this, "last-pb-context-exited", true);
621
622 mPermissionService = do_GetService(NS_COOKIEPERMISSION_CONTRACTID);
623 if (!mPermissionService) {
624 NS_WARNING(
625 "nsICookiePermission implementation not available - some features "
626 "won't work!");
627 COOKIE_LOGSTRING(
628 LogLevel::Warning,
629 ("Init(): nsICookiePermission implementation not available"));
630 }
631
632 return NS_OK;
633 }
634
InitDBStates()635 void nsCookieService::InitDBStates() {
636 NS_ASSERTION(!mDBState, "already have a DBState");
637 NS_ASSERTION(!mDefaultDBState, "already have a default DBState");
638 NS_ASSERTION(!mPrivateDBState, "already have a private DBState");
639 NS_ASSERTION(!mInitializedDBStates, "already initialized");
640 NS_ASSERTION(!mThread, "already have a cookie thread");
641
642 // Create a new default DBState and set our current one.
643 mDefaultDBState = new DBState();
644 mDBState = mDefaultDBState;
645
646 mPrivateDBState = new DBState();
647
648 // Get our cookie file.
649 nsresult rv = NS_GetSpecialDirectory(
650 NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mDefaultDBState->cookieFile));
651 if (NS_FAILED(rv)) {
652 // We've already set up our DBStates appropriately; nothing more to do.
653 COOKIE_LOGSTRING(LogLevel::Warning,
654 ("InitDBStates(): couldn't get cookie file"));
655
656 mInitializedDBConn = true;
657 mInitializedDBStates = true;
658 return;
659 }
660 mDefaultDBState->cookieFile->AppendNative(NS_LITERAL_CSTRING(COOKIES_FILE));
661
662 NS_ENSURE_SUCCESS_VOID(NS_NewNamedThread("Cookie", getter_AddRefs(mThread)));
663
664 nsCOMPtr<nsIRunnable> runnable =
665 NS_NewRunnableFunction("InitDBStates.TryInitDB", [] {
666 NS_ENSURE_TRUE_VOID(gCookieService && gCookieService->mDBState &&
667 gCookieService->mDefaultDBState);
668
669 MonitorAutoLock lock(gCookieService->mMonitor);
670
671 // Attempt to open and read the database. If TryInitDB() returns
672 // RESULT_RETRY, do so.
673 OpenDBResult result = gCookieService->TryInitDB(false);
674 if (result == RESULT_RETRY) {
675 // Database may be corrupt. Synchronously close the connection, clean
676 // up the default DBState, and try again.
677 COOKIE_LOGSTRING(LogLevel::Warning,
678 ("InitDBStates(): retrying TryInitDB()"));
679 gCookieService->CleanupCachedStatements();
680 gCookieService->CleanupDefaultDBConnection();
681 result = gCookieService->TryInitDB(true);
682 if (result == RESULT_RETRY) {
683 // We're done. Change the code to failure so we clean up below.
684 result = RESULT_FAILURE;
685 }
686 }
687
688 if (result == RESULT_FAILURE) {
689 COOKIE_LOGSTRING(
690 LogLevel::Warning,
691 ("InitDBStates(): TryInitDB() failed, closing connection"));
692
693 // Connection failure is unrecoverable. Clean up our connection. We
694 // can run fine without persistent storage -- e.g. if there's no
695 // profile.
696 gCookieService->CleanupCachedStatements();
697 gCookieService->CleanupDefaultDBConnection();
698
699 // No need to initialize dbConn
700 gCookieService->mInitializedDBConn = true;
701 }
702
703 gCookieService->mInitializedDBStates = true;
704
705 NS_DispatchToMainThread(
706 NS_NewRunnableFunction("TryInitDB.InitDBConn", [] {
707 NS_ENSURE_TRUE_VOID(gCookieService);
708 gCookieService->InitDBConn();
709 }));
710 gCookieService->mMonitor.Notify();
711 });
712
713 mThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
714 }
715
716 namespace {
717
718 class ConvertAppIdToOriginAttrsSQLFunction final : public mozIStorageFunction {
719 ~ConvertAppIdToOriginAttrsSQLFunction() = default;
720
721 NS_DECL_ISUPPORTS
722 NS_DECL_MOZISTORAGEFUNCTION
723 };
724
725 NS_IMPL_ISUPPORTS(ConvertAppIdToOriginAttrsSQLFunction, mozIStorageFunction);
726
727 NS_IMETHODIMP
OnFunctionCall(mozIStorageValueArray * aFunctionArguments,nsIVariant ** aResult)728 ConvertAppIdToOriginAttrsSQLFunction::OnFunctionCall(
729 mozIStorageValueArray *aFunctionArguments, nsIVariant **aResult) {
730 nsresult rv;
731 int32_t inIsolatedMozBrowser;
732
733 rv = aFunctionArguments->GetInt32(1, &inIsolatedMozBrowser);
734 NS_ENSURE_SUCCESS(rv, rv);
735
736 // Create an originAttributes object by inIsolatedMozBrowser.
737 // Then create the originSuffix string from this object.
738 OriginAttributes attrs(nsIScriptSecurityManager::NO_APP_ID,
739 (inIsolatedMozBrowser ? true : false));
740 nsAutoCString suffix;
741 attrs.CreateSuffix(suffix);
742
743 RefPtr<nsVariant> outVar(new nsVariant());
744 rv = outVar->SetAsAUTF8String(suffix);
745 NS_ENSURE_SUCCESS(rv, rv);
746
747 outVar.forget(aResult);
748 return NS_OK;
749 }
750
751 class SetAppIdFromOriginAttributesSQLFunction final
752 : public mozIStorageFunction {
753 ~SetAppIdFromOriginAttributesSQLFunction() = default;
754
755 NS_DECL_ISUPPORTS
756 NS_DECL_MOZISTORAGEFUNCTION
757 };
758
759 NS_IMPL_ISUPPORTS(SetAppIdFromOriginAttributesSQLFunction, mozIStorageFunction);
760
761 NS_IMETHODIMP
OnFunctionCall(mozIStorageValueArray * aFunctionArguments,nsIVariant ** aResult)762 SetAppIdFromOriginAttributesSQLFunction::OnFunctionCall(
763 mozIStorageValueArray *aFunctionArguments, nsIVariant **aResult) {
764 nsresult rv;
765 nsAutoCString suffix;
766 OriginAttributes attrs;
767
768 rv = aFunctionArguments->GetUTF8String(0, suffix);
769 NS_ENSURE_SUCCESS(rv, rv);
770 bool success = attrs.PopulateFromSuffix(suffix);
771 NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
772
773 RefPtr<nsVariant> outVar(new nsVariant());
774 rv = outVar->SetAsInt32(attrs.mAppId);
775 NS_ENSURE_SUCCESS(rv, rv);
776
777 outVar.forget(aResult);
778 return NS_OK;
779 }
780
781 class SetInBrowserFromOriginAttributesSQLFunction final
782 : public mozIStorageFunction {
783 ~SetInBrowserFromOriginAttributesSQLFunction() = default;
784
785 NS_DECL_ISUPPORTS
786 NS_DECL_MOZISTORAGEFUNCTION
787 };
788
789 NS_IMPL_ISUPPORTS(SetInBrowserFromOriginAttributesSQLFunction,
790 mozIStorageFunction);
791
792 NS_IMETHODIMP
OnFunctionCall(mozIStorageValueArray * aFunctionArguments,nsIVariant ** aResult)793 SetInBrowserFromOriginAttributesSQLFunction::OnFunctionCall(
794 mozIStorageValueArray *aFunctionArguments, nsIVariant **aResult) {
795 nsresult rv;
796 nsAutoCString suffix;
797 OriginAttributes attrs;
798
799 rv = aFunctionArguments->GetUTF8String(0, suffix);
800 NS_ENSURE_SUCCESS(rv, rv);
801 bool success = attrs.PopulateFromSuffix(suffix);
802 NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
803
804 RefPtr<nsVariant> outVar(new nsVariant());
805 rv = outVar->SetAsInt32(attrs.mInIsolatedMozBrowser);
806 NS_ENSURE_SUCCESS(rv, rv);
807
808 outVar.forget(aResult);
809 return NS_OK;
810 }
811
812 } // namespace
813
814 /* Attempt to open and read the database. If 'aRecreateDB' is true, try to
815 * move the existing database file out of the way and create a new one.
816 *
817 * @returns RESULT_OK if opening or creating the database succeeded;
818 * RESULT_RETRY if the database cannot be opened, is corrupt, or some
819 * other failure occurred that might be resolved by recreating the
820 * database; or RESULT_FAILED if there was an unrecoverable error and
821 * we must run without a database.
822 *
823 * If RESULT_RETRY or RESULT_FAILED is returned, the caller should perform
824 * cleanup of the default DBState.
825 */
TryInitDB(bool aRecreateDB)826 OpenDBResult nsCookieService::TryInitDB(bool aRecreateDB) {
827 NS_ASSERTION(!mDefaultDBState->dbConn, "nonnull dbConn");
828 NS_ASSERTION(!mDefaultDBState->stmtInsert, "nonnull stmtInsert");
829 NS_ASSERTION(!mDefaultDBState->insertListener, "nonnull insertListener");
830 NS_ASSERTION(!mDefaultDBState->syncConn, "nonnull syncConn");
831 NS_ASSERTION(NS_GetCurrentThread() == mThread, "non cookie thread");
832
833 // Ditch an existing db, if we've been told to (i.e. it's corrupt). We don't
834 // want to delete it outright, since it may be useful for debugging purposes,
835 // so we move it out of the way.
836 nsresult rv;
837 if (aRecreateDB) {
838 nsCOMPtr<nsIFile> backupFile;
839 mDefaultDBState->cookieFile->Clone(getter_AddRefs(backupFile));
840 rv = backupFile->MoveToNative(nullptr,
841 NS_LITERAL_CSTRING(COOKIES_FILE ".bak"));
842 NS_ENSURE_SUCCESS(rv, RESULT_FAILURE);
843 }
844
845 // This block provides scope for the Telemetry AutoTimer
846 {
847 Telemetry::AutoTimer<Telemetry::MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS>
848 telemetry;
849 ReadAheadFile(mDefaultDBState->cookieFile);
850
851 // open a connection to the cookie database, and only cache our connection
852 // and statements upon success. The connection is opened unshared to
853 // eliminate cache contention between the main and background threads.
854 rv = mStorageService->OpenUnsharedDatabase(
855 mDefaultDBState->cookieFile, getter_AddRefs(mDefaultDBState->syncConn));
856 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
857 }
858
859 auto guard = MakeScopeExit([&] { mDefaultDBState->syncConn = nullptr; });
860
861 bool tableExists = false;
862 mDefaultDBState->syncConn->TableExists(NS_LITERAL_CSTRING("moz_cookies"),
863 &tableExists);
864 if (!tableExists) {
865 rv = CreateTable();
866 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
867
868 } else {
869 // table already exists; check the schema version before reading
870 int32_t dbSchemaVersion;
871 rv = mDefaultDBState->syncConn->GetSchemaVersion(&dbSchemaVersion);
872 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
873
874 // Start a transaction for the whole migration block.
875 mozStorageTransaction transaction(mDefaultDBState->syncConn, true);
876
877 switch (dbSchemaVersion) {
878 // Upgrading.
879 // Every time you increment the database schema, you need to implement
880 // the upgrading code from the previous version to the new one. If
881 // migration fails for any reason, it's a bug -- so we return RESULT_RETRY
882 // such that the original database will be saved, in the hopes that we
883 // might one day see it and fix it.
884 case 1: {
885 // Add the lastAccessed column to the table.
886 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
887 "ALTER TABLE moz_cookies ADD lastAccessed INTEGER"));
888 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
889 }
890 // Fall through to the next upgrade.
891 MOZ_FALLTHROUGH;
892
893 case 2: {
894 // Add the baseDomain column and index to the table.
895 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
896 NS_LITERAL_CSTRING("ALTER TABLE moz_cookies ADD baseDomain TEXT"));
897 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
898
899 // Compute the baseDomains for the table. This must be done eagerly
900 // otherwise we won't be able to synchronously read in individual
901 // domains on demand.
902 const int64_t SCHEMA2_IDX_ID = 0;
903 const int64_t SCHEMA2_IDX_HOST = 1;
904 nsCOMPtr<mozIStorageStatement> select;
905 rv = mDefaultDBState->syncConn->CreateStatement(
906 NS_LITERAL_CSTRING("SELECT id, host FROM moz_cookies"),
907 getter_AddRefs(select));
908 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
909
910 nsCOMPtr<mozIStorageStatement> update;
911 rv = mDefaultDBState->syncConn->CreateStatement(
912 NS_LITERAL_CSTRING("UPDATE moz_cookies SET baseDomain = "
913 ":baseDomain WHERE id = :id"),
914 getter_AddRefs(update));
915 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
916
917 nsCString baseDomain, host;
918 bool hasResult;
919 while (true) {
920 rv = select->ExecuteStep(&hasResult);
921 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
922
923 if (!hasResult) break;
924
925 int64_t id = select->AsInt64(SCHEMA2_IDX_ID);
926 select->GetUTF8String(SCHEMA2_IDX_HOST, host);
927
928 rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
929 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
930
931 mozStorageStatementScoper scoper(update);
932
933 rv = update->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"),
934 baseDomain);
935 NS_ASSERT_SUCCESS(rv);
936 rv = update->BindInt64ByName(NS_LITERAL_CSTRING("id"), id);
937 NS_ASSERT_SUCCESS(rv);
938
939 rv = update->ExecuteStep(&hasResult);
940 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
941 }
942
943 // Create an index on baseDomain.
944 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
945 "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)"));
946 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
947 }
948 // Fall through to the next upgrade.
949 MOZ_FALLTHROUGH;
950
951 case 3: {
952 // Add the creationTime column to the table, and create a unique index
953 // on (name, host, path). Before we do this, we have to purge the table
954 // of expired cookies such that we know that the (name, host, path)
955 // index is truly unique -- otherwise we can't create the index. Note
956 // that we can't just execute a statement to delete all rows where the
957 // expiry column is in the past -- doing so would rely on the clock
958 // (both now and when previous cookies were set) being monotonic.
959
960 // Select the whole table, and order by the fields we're interested in.
961 // This means we can simply do a linear traversal of the results and
962 // check for duplicates as we go.
963 const int64_t SCHEMA3_IDX_ID = 0;
964 const int64_t SCHEMA3_IDX_NAME = 1;
965 const int64_t SCHEMA3_IDX_HOST = 2;
966 const int64_t SCHEMA3_IDX_PATH = 3;
967 nsCOMPtr<mozIStorageStatement> select;
968 rv = mDefaultDBState->syncConn->CreateStatement(
969 NS_LITERAL_CSTRING(
970 "SELECT id, name, host, path FROM moz_cookies "
971 "ORDER BY name ASC, host ASC, path ASC, expiry ASC"),
972 getter_AddRefs(select));
973 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
974
975 nsCOMPtr<mozIStorageStatement> deleteExpired;
976 rv = mDefaultDBState->syncConn->CreateStatement(
977 NS_LITERAL_CSTRING("DELETE FROM moz_cookies WHERE id = :id"),
978 getter_AddRefs(deleteExpired));
979 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
980
981 // Read the first row.
982 bool hasResult;
983 rv = select->ExecuteStep(&hasResult);
984 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
985
986 if (hasResult) {
987 nsCString name1, host1, path1;
988 int64_t id1 = select->AsInt64(SCHEMA3_IDX_ID);
989 select->GetUTF8String(SCHEMA3_IDX_NAME, name1);
990 select->GetUTF8String(SCHEMA3_IDX_HOST, host1);
991 select->GetUTF8String(SCHEMA3_IDX_PATH, path1);
992
993 nsCString name2, host2, path2;
994 while (true) {
995 // Read the second row.
996 rv = select->ExecuteStep(&hasResult);
997 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
998
999 if (!hasResult) break;
1000
1001 int64_t id2 = select->AsInt64(SCHEMA3_IDX_ID);
1002 select->GetUTF8String(SCHEMA3_IDX_NAME, name2);
1003 select->GetUTF8String(SCHEMA3_IDX_HOST, host2);
1004 select->GetUTF8String(SCHEMA3_IDX_PATH, path2);
1005
1006 // If the two rows match in (name, host, path), we know the earlier
1007 // row has an earlier expiry time. Delete it.
1008 if (name1 == name2 && host1 == host2 && path1 == path2) {
1009 mozStorageStatementScoper scoper(deleteExpired);
1010
1011 rv =
1012 deleteExpired->BindInt64ByName(NS_LITERAL_CSTRING("id"), id1);
1013 NS_ASSERT_SUCCESS(rv);
1014
1015 rv = deleteExpired->ExecuteStep(&hasResult);
1016 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1017 }
1018
1019 // Make the second row the first for the next iteration.
1020 name1 = name2;
1021 host1 = host2;
1022 path1 = path2;
1023 id1 = id2;
1024 }
1025 }
1026
1027 // Add the creationTime column to the table.
1028 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1029 "ALTER TABLE moz_cookies ADD creationTime INTEGER"));
1030 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1031
1032 // Copy the id of each row into the new creationTime column.
1033 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1034 NS_LITERAL_CSTRING("UPDATE moz_cookies SET creationTime = "
1035 "(SELECT id WHERE id = moz_cookies.id)"));
1036 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1037
1038 // Create a unique index on (name, host, path) to allow fast lookup.
1039 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1040 NS_LITERAL_CSTRING("CREATE UNIQUE INDEX moz_uniqueid "
1041 "ON moz_cookies (name, host, path)"));
1042 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1043 }
1044 // Fall through to the next upgrade.
1045 MOZ_FALLTHROUGH;
1046
1047 case 4: {
1048 // We need to add appId/inBrowserElement, plus change a constraint on
1049 // the table (unique entries now include appId/inBrowserElement):
1050 // this requires creating a new table and copying the data to it. We
1051 // then rename the new table to the old name.
1052 //
1053 // Why we made this change: appId/inBrowserElement allow "cookie jars"
1054 // for Firefox OS. We create a separate cookie namespace per {appId,
1055 // inBrowserElement}. When upgrading, we convert existing cookies
1056 // (which imply we're on desktop/mobile) to use {0, false}, as that is
1057 // the only namespace used by a non-Firefox-OS implementation.
1058
1059 // Rename existing table
1060 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1061 "ALTER TABLE moz_cookies RENAME TO moz_cookies_old"));
1062 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1063
1064 // Drop existing index (CreateTable will create new one for new table)
1065 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1066 NS_LITERAL_CSTRING("DROP INDEX moz_basedomain"));
1067 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1068
1069 // Create new table (with new fields and new unique constraint)
1070 rv = CreateTableForSchemaVersion5();
1071 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1072
1073 // Copy data from old table, using appId/inBrowser=0 for existing rows
1074 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1075 "INSERT INTO moz_cookies "
1076 "(baseDomain, appId, inBrowserElement, name, value, host, path, "
1077 "expiry,"
1078 " lastAccessed, creationTime, isSecure, isHttpOnly) "
1079 "SELECT baseDomain, 0, 0, name, value, host, path, expiry,"
1080 " lastAccessed, creationTime, isSecure, isHttpOnly "
1081 "FROM moz_cookies_old"));
1082 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1083
1084 // Drop old table
1085 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1086 NS_LITERAL_CSTRING("DROP TABLE moz_cookies_old"));
1087 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1088
1089 COOKIE_LOGSTRING(LogLevel::Debug,
1090 ("Upgraded database to schema version 5"));
1091 }
1092 // Fall through to the next upgrade.
1093 MOZ_FALLTHROUGH;
1094
1095 case 5: {
1096 // Change in the version: Replace the columns |appId| and
1097 // |inBrowserElement| by a single column |originAttributes|.
1098 //
1099 // Why we made this change: FxOS new security model (NSec) encapsulates
1100 // "appId/inIsolatedMozBrowser" in nsIPrincipal::originAttributes to
1101 // make it easier to modify the contents of this structure in the
1102 // future.
1103 //
1104 // We do the migration in several steps:
1105 // 1. Rename the old table.
1106 // 2. Create a new table.
1107 // 3. Copy data from the old table to the new table; convert appId and
1108 // inBrowserElement to originAttributes in the meantime.
1109
1110 // Rename existing table.
1111 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1112 "ALTER TABLE moz_cookies RENAME TO moz_cookies_old"));
1113 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1114
1115 // Drop existing index (CreateTable will create new one for new table).
1116 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1117 NS_LITERAL_CSTRING("DROP INDEX moz_basedomain"));
1118 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1119
1120 // Create new table with new fields and new unique constraint.
1121 rv = CreateTableForSchemaVersion6();
1122 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1123
1124 // Copy data from old table without the two deprecated columns appId and
1125 // inBrowserElement.
1126 nsCOMPtr<mozIStorageFunction> convertToOriginAttrs(
1127 new ConvertAppIdToOriginAttrsSQLFunction());
1128 NS_ENSURE_TRUE(convertToOriginAttrs, RESULT_RETRY);
1129
1130 NS_NAMED_LITERAL_CSTRING(convertToOriginAttrsName,
1131 "CONVERT_TO_ORIGIN_ATTRIBUTES");
1132
1133 rv = mDefaultDBState->syncConn->CreateFunction(convertToOriginAttrsName,
1134 2, convertToOriginAttrs);
1135 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1136
1137 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1138 "INSERT INTO moz_cookies "
1139 "(baseDomain, originAttributes, name, value, host, path, expiry,"
1140 " lastAccessed, creationTime, isSecure, isHttpOnly) "
1141 "SELECT baseDomain, "
1142 " CONVERT_TO_ORIGIN_ATTRIBUTES(appId, inBrowserElement),"
1143 " name, value, host, path, expiry, lastAccessed, creationTime, "
1144 " isSecure, isHttpOnly "
1145 "FROM moz_cookies_old"));
1146 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1147
1148 rv =
1149 mDefaultDBState->syncConn->RemoveFunction(convertToOriginAttrsName);
1150 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1151
1152 // Drop old table
1153 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1154 NS_LITERAL_CSTRING("DROP TABLE moz_cookies_old"));
1155 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1156
1157 COOKIE_LOGSTRING(LogLevel::Debug,
1158 ("Upgraded database to schema version 6"));
1159 }
1160 MOZ_FALLTHROUGH;
1161
1162 case 6: {
1163 // We made a mistake in schema version 6. We cannot remove expected
1164 // columns of any version (checked in the default case) from cookie
1165 // database, because doing this would destroy the possibility of
1166 // downgrading database.
1167 //
1168 // This version simply restores appId and inBrowserElement columns in
1169 // order to fix downgrading issue even though these two columns are no
1170 // longer used in the latest schema.
1171 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1172 "ALTER TABLE moz_cookies ADD appId INTEGER DEFAULT 0;"));
1173 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1174
1175 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1176 "ALTER TABLE moz_cookies ADD inBrowserElement INTEGER DEFAULT 0;"));
1177 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1178
1179 // Compute and populate the values of appId and inBrwoserElement from
1180 // originAttributes.
1181 nsCOMPtr<mozIStorageFunction> setAppId(
1182 new SetAppIdFromOriginAttributesSQLFunction());
1183 NS_ENSURE_TRUE(setAppId, RESULT_RETRY);
1184
1185 NS_NAMED_LITERAL_CSTRING(setAppIdName, "SET_APP_ID");
1186
1187 rv = mDefaultDBState->syncConn->CreateFunction(setAppIdName, 1,
1188 setAppId);
1189 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1190
1191 nsCOMPtr<mozIStorageFunction> setInBrowser(
1192 new SetInBrowserFromOriginAttributesSQLFunction());
1193 NS_ENSURE_TRUE(setInBrowser, RESULT_RETRY);
1194
1195 NS_NAMED_LITERAL_CSTRING(setInBrowserName, "SET_IN_BROWSER");
1196
1197 rv = mDefaultDBState->syncConn->CreateFunction(setInBrowserName, 1,
1198 setInBrowser);
1199 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1200
1201 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1202 "UPDATE moz_cookies SET appId = SET_APP_ID(originAttributes), "
1203 "inBrowserElement = SET_IN_BROWSER(originAttributes);"));
1204 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1205
1206 rv = mDefaultDBState->syncConn->RemoveFunction(setAppIdName);
1207 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1208
1209 rv = mDefaultDBState->syncConn->RemoveFunction(setInBrowserName);
1210 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1211
1212 COOKIE_LOGSTRING(LogLevel::Debug,
1213 ("Upgraded database to schema version 7"));
1214 }
1215 MOZ_FALLTHROUGH;
1216
1217 case 7: {
1218 // Remove the appId field from moz_cookies.
1219 //
1220 // Unfortunately sqlite doesn't support dropping columns using ALTER
1221 // TABLE, so we need to go through the procedure documented in
1222 // https://www.sqlite.org/lang_altertable.html.
1223
1224 // Drop existing index
1225 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1226 NS_LITERAL_CSTRING("DROP INDEX moz_basedomain"));
1227 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1228
1229 // Create a new_moz_cookies table without the appId field.
1230 rv = CreateTableWorker("new_moz_cookies");
1231 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1232
1233 // Move the data over.
1234 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1235 NS_LITERAL_CSTRING("INSERT INTO new_moz_cookies ("
1236 "id, "
1237 "baseDomain, "
1238 "originAttributes, "
1239 "name, "
1240 "value, "
1241 "host, "
1242 "path, "
1243 "expiry, "
1244 "lastAccessed, "
1245 "creationTime, "
1246 "isSecure, "
1247 "isHttpOnly, "
1248 "inBrowserElement "
1249 ") SELECT "
1250 "id, "
1251 "baseDomain, "
1252 "originAttributes, "
1253 "name, "
1254 "value, "
1255 "host, "
1256 "path, "
1257 "expiry, "
1258 "lastAccessed, "
1259 "creationTime, "
1260 "isSecure, "
1261 "isHttpOnly, "
1262 "inBrowserElement "
1263 "FROM moz_cookies;"));
1264 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1265
1266 // Drop the old table
1267 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1268 NS_LITERAL_CSTRING("DROP TABLE moz_cookies;"));
1269 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1270
1271 // Rename new_moz_cookies to moz_cookies.
1272 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1273 "ALTER TABLE new_moz_cookies RENAME TO moz_cookies;"));
1274 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1275
1276 // Recreate our index.
1277 rv = CreateIndex();
1278 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1279
1280 COOKIE_LOGSTRING(LogLevel::Debug,
1281 ("Upgraded database to schema version 8"));
1282 }
1283 MOZ_FALLTHROUGH;
1284
1285 case 8: {
1286 // Add the sameSite column to the table.
1287 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1288 NS_LITERAL_CSTRING("ALTER TABLE moz_cookies ADD sameSite INTEGER"));
1289 COOKIE_LOGSTRING(LogLevel::Debug,
1290 ("Upgraded database to schema version 9"));
1291 }
1292
1293 // No more upgrades. Update the schema version.
1294 rv =
1295 mDefaultDBState->syncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
1296 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1297
1298 Telemetry::Accumulate(Telemetry::MOZ_SQLITE_COOKIES_OLD_SCHEMA,
1299 dbSchemaVersion);
1300 MOZ_FALLTHROUGH;
1301
1302 case COOKIES_SCHEMA_VERSION:
1303 break;
1304
1305 case 0: {
1306 NS_WARNING("couldn't get schema version!");
1307
1308 // the table may be usable; someone might've just clobbered the schema
1309 // version. we can treat this case like a downgrade using the codepath
1310 // below, by verifying the columns we care about are all there. for now,
1311 // re-set the schema version in the db, in case the checks succeed (if
1312 // they don't, we're dropping the table anyway).
1313 rv =
1314 mDefaultDBState->syncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
1315 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1316 }
1317 // fall through to downgrade check
1318 MOZ_FALLTHROUGH;
1319
1320 // downgrading.
1321 // if columns have been added to the table, we can still use the ones we
1322 // understand safely. if columns have been deleted or altered, just
1323 // blow away the table and start from scratch! if you change the way
1324 // a column is interpreted, make sure you also change its name so this
1325 // check will catch it.
1326 default: {
1327 // check if all the expected columns exist
1328 nsCOMPtr<mozIStorageStatement> stmt;
1329 rv = mDefaultDBState->syncConn->CreateStatement(
1330 NS_LITERAL_CSTRING("SELECT "
1331 "id, "
1332 "baseDomain, "
1333 "originAttributes, "
1334 "name, "
1335 "value, "
1336 "host, "
1337 "path, "
1338 "expiry, "
1339 "lastAccessed, "
1340 "creationTime, "
1341 "isSecure, "
1342 "isHttpOnly, "
1343 "sameSite "
1344 "FROM moz_cookies"),
1345 getter_AddRefs(stmt));
1346 if (NS_SUCCEEDED(rv)) break;
1347
1348 // our columns aren't there - drop the table!
1349 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1350 NS_LITERAL_CSTRING("DROP TABLE moz_cookies"));
1351 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1352
1353 rv = CreateTable();
1354 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
1355 } break;
1356 }
1357 }
1358
1359 // if we deleted a corrupt db, don't attempt to import - return now
1360 if (aRecreateDB) {
1361 return RESULT_OK;
1362 }
1363
1364 // check whether to import or just read in the db
1365 if (tableExists) {
1366 return Read();
1367 }
1368
1369 nsCOMPtr<nsIRunnable> runnable =
1370 NS_NewRunnableFunction("TryInitDB.ImportCookies", [] {
1371 NS_ENSURE_TRUE_VOID(gCookieService);
1372 NS_ENSURE_TRUE_VOID(gCookieService->mDefaultDBState);
1373 nsCOMPtr<nsIFile> oldCookieFile;
1374 nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
1375 getter_AddRefs(oldCookieFile));
1376 if (NS_FAILED(rv)) {
1377 return;
1378 }
1379
1380 // Import cookies, and clean up the old file regardless of success or
1381 // failure. Note that we have to switch out our DBState temporarily, in
1382 // case we're in private browsing mode; otherwise ImportCookies() won't
1383 // be happy.
1384 DBState *initialState = gCookieService->mDBState;
1385 gCookieService->mDBState = gCookieService->mDefaultDBState;
1386 oldCookieFile->AppendNative(NS_LITERAL_CSTRING(OLD_COOKIE_FILE_NAME));
1387 gCookieService->ImportCookies(oldCookieFile);
1388 oldCookieFile->Remove(false);
1389 gCookieService->mDBState = initialState;
1390 });
1391
1392 NS_DispatchToMainThread(runnable);
1393
1394 return RESULT_OK;
1395 }
1396
InitDBConn()1397 void nsCookieService::InitDBConn() {
1398 MOZ_ASSERT(NS_IsMainThread());
1399
1400 // We should skip InitDBConn if we close profile during initializing DBStates
1401 // and then InitDBConn is called after we close the DBStates.
1402 if (!mInitializedDBStates || mInitializedDBConn || !mDefaultDBState) {
1403 return;
1404 }
1405
1406 for (uint32_t i = 0; i < mReadArray.Length(); ++i) {
1407 CookieDomainTuple &tuple = mReadArray[i];
1408 RefPtr<nsCookie> cookie = nsCookie::Create(
1409 tuple.cookie->name, tuple.cookie->value, tuple.cookie->host,
1410 tuple.cookie->path, tuple.cookie->expiry, tuple.cookie->lastAccessed,
1411 tuple.cookie->creationTime, false, tuple.cookie->isSecure,
1412 tuple.cookie->isHttpOnly, tuple.cookie->originAttributes,
1413 tuple.cookie->sameSite);
1414
1415 AddCookieToList(tuple.key, cookie, mDefaultDBState, nullptr, false);
1416 }
1417
1418 if (NS_FAILED(InitDBConnInternal())) {
1419 COOKIE_LOGSTRING(LogLevel::Warning,
1420 ("InitDBConn(): retrying InitDBConnInternal()"));
1421 CleanupCachedStatements();
1422 CleanupDefaultDBConnection();
1423 if (NS_FAILED(InitDBConnInternal())) {
1424 COOKIE_LOGSTRING(
1425 LogLevel::Warning,
1426 ("InitDBConn(): InitDBConnInternal() failed, closing connection"));
1427
1428 // Game over, clean the connections.
1429 CleanupCachedStatements();
1430 CleanupDefaultDBConnection();
1431 }
1432 }
1433 mInitializedDBConn = true;
1434
1435 COOKIE_LOGSTRING(LogLevel::Debug,
1436 ("InitDBConn(): mInitializedDBConn = true"));
1437 mEndInitDBConn = mozilla::TimeStamp::Now();
1438
1439 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
1440 if (os) {
1441 os->NotifyObservers(nullptr, "cookie-db-read", nullptr);
1442 mReadArray.Clear();
1443 }
1444 }
1445
InitDBConnInternal()1446 nsresult nsCookieService::InitDBConnInternal() {
1447 MOZ_ASSERT(NS_IsMainThread());
1448
1449 nsresult rv = mStorageService->OpenUnsharedDatabase(
1450 mDefaultDBState->cookieFile, getter_AddRefs(mDefaultDBState->dbConn));
1451 NS_ENSURE_SUCCESS(rv, rv);
1452
1453 // Set up our listeners.
1454 mDefaultDBState->insertListener = new InsertCookieDBListener(mDefaultDBState);
1455 mDefaultDBState->updateListener = new UpdateCookieDBListener(mDefaultDBState);
1456 mDefaultDBState->removeListener = new RemoveCookieDBListener(mDefaultDBState);
1457 mDefaultDBState->closeListener = new CloseCookieDBListener(mDefaultDBState);
1458
1459 // Grow cookie db in 512KB increments
1460 mDefaultDBState->dbConn->SetGrowthIncrement(512 * 1024, EmptyCString());
1461
1462 // make operations on the table asynchronous, for performance
1463 mDefaultDBState->dbConn->ExecuteSimpleSQL(
1464 NS_LITERAL_CSTRING("PRAGMA synchronous = OFF"));
1465
1466 // Use write-ahead-logging for performance. We cap the autocheckpoint limit at
1467 // 16 pages (around 500KB).
1468 mDefaultDBState->dbConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1469 MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA journal_mode = WAL"));
1470 mDefaultDBState->dbConn->ExecuteSimpleSQL(
1471 NS_LITERAL_CSTRING("PRAGMA wal_autocheckpoint = 16"));
1472
1473 // cache frequently used statements (for insertion, deletion, and updating)
1474 rv = mDefaultDBState->dbConn->CreateAsyncStatement(
1475 NS_LITERAL_CSTRING("INSERT INTO moz_cookies ("
1476 "baseDomain, "
1477 "originAttributes, "
1478 "name, "
1479 "value, "
1480 "host, "
1481 "path, "
1482 "expiry, "
1483 "lastAccessed, "
1484 "creationTime, "
1485 "isSecure, "
1486 "isHttpOnly, "
1487 "sameSite "
1488 ") VALUES ("
1489 ":baseDomain, "
1490 ":originAttributes, "
1491 ":name, "
1492 ":value, "
1493 ":host, "
1494 ":path, "
1495 ":expiry, "
1496 ":lastAccessed, "
1497 ":creationTime, "
1498 ":isSecure, "
1499 ":isHttpOnly, "
1500 ":sameSite"
1501 ")"),
1502 getter_AddRefs(mDefaultDBState->stmtInsert));
1503 NS_ENSURE_SUCCESS(rv, rv);
1504
1505 rv = mDefaultDBState->dbConn->CreateAsyncStatement(
1506 NS_LITERAL_CSTRING("DELETE FROM moz_cookies "
1507 "WHERE name = :name AND host = :host AND path = :path "
1508 "AND originAttributes = :originAttributes"),
1509 getter_AddRefs(mDefaultDBState->stmtDelete));
1510 NS_ENSURE_SUCCESS(rv, rv);
1511
1512 rv = mDefaultDBState->dbConn->CreateAsyncStatement(
1513 NS_LITERAL_CSTRING("UPDATE moz_cookies SET lastAccessed = :lastAccessed "
1514 "WHERE name = :name AND host = :host AND path = :path "
1515 "AND originAttributes = :originAttributes"),
1516 getter_AddRefs(mDefaultDBState->stmtUpdate));
1517 return rv;
1518 }
1519
1520 // Sets the schema version and creates the moz_cookies table.
CreateTableWorker(const char * aName)1521 nsresult nsCookieService::CreateTableWorker(const char *aName) {
1522 // Create the table.
1523 // We default originAttributes to empty string: this is so if users revert to
1524 // an older Firefox version that doesn't know about this field, any cookies
1525 // set will still work once they upgrade back.
1526 nsAutoCString command("CREATE TABLE ");
1527 command.Append(aName);
1528 command.AppendLiteral(
1529 " ("
1530 "id INTEGER PRIMARY KEY, "
1531 "baseDomain TEXT, "
1532 "originAttributes TEXT NOT NULL DEFAULT '', "
1533 "name TEXT, "
1534 "value TEXT, "
1535 "host TEXT, "
1536 "path TEXT, "
1537 "expiry INTEGER, "
1538 "lastAccessed INTEGER, "
1539 "creationTime INTEGER, "
1540 "isSecure INTEGER, "
1541 "isHttpOnly INTEGER, "
1542 "inBrowserElement INTEGER DEFAULT 0, "
1543 "sameSite INTEGER DEFAULT 0, "
1544 "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)"
1545 ")");
1546 return mDefaultDBState->syncConn->ExecuteSimpleSQL(command);
1547 }
1548
1549 // Sets the schema version and creates the moz_cookies table.
CreateTable()1550 nsresult nsCookieService::CreateTable() {
1551 // Set the schema version, before creating the table.
1552 nsresult rv =
1553 mDefaultDBState->syncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
1554 if (NS_FAILED(rv)) return rv;
1555
1556 rv = CreateTableWorker("moz_cookies");
1557 if (NS_FAILED(rv)) return rv;
1558
1559 return CreateIndex();
1560 }
1561
CreateIndex()1562 nsresult nsCookieService::CreateIndex() {
1563 // Create an index on baseDomain.
1564 return mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1565 "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
1566 "originAttributes)"));
1567 }
1568
1569 // Sets the schema version and creates the moz_cookies table.
CreateTableForSchemaVersion6()1570 nsresult nsCookieService::CreateTableForSchemaVersion6() {
1571 // Set the schema version, before creating the table.
1572 nsresult rv = mDefaultDBState->syncConn->SetSchemaVersion(6);
1573 if (NS_FAILED(rv)) return rv;
1574
1575 // Create the table.
1576 // We default originAttributes to empty string: this is so if users revert to
1577 // an older Firefox version that doesn't know about this field, any cookies
1578 // set will still work once they upgrade back.
1579 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1580 "CREATE TABLE moz_cookies ("
1581 "id INTEGER PRIMARY KEY, "
1582 "baseDomain TEXT, "
1583 "originAttributes TEXT NOT NULL DEFAULT '', "
1584 "name TEXT, "
1585 "value TEXT, "
1586 "host TEXT, "
1587 "path TEXT, "
1588 "expiry INTEGER, "
1589 "lastAccessed INTEGER, "
1590 "creationTime INTEGER, "
1591 "isSecure INTEGER, "
1592 "isHttpOnly INTEGER, "
1593 "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)"
1594 ")"));
1595 if (NS_FAILED(rv)) return rv;
1596
1597 // Create an index on baseDomain.
1598 return mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1599 "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
1600 "originAttributes)"));
1601 }
1602
1603 // Sets the schema version and creates the moz_cookies table.
CreateTableForSchemaVersion5()1604 nsresult nsCookieService::CreateTableForSchemaVersion5() {
1605 // Set the schema version, before creating the table.
1606 nsresult rv = mDefaultDBState->syncConn->SetSchemaVersion(5);
1607 if (NS_FAILED(rv)) return rv;
1608
1609 // Create the table. We default appId/inBrowserElement to 0: this is so if
1610 // users revert to an older Firefox version that doesn't know about these
1611 // fields, any cookies set will still work once they upgrade back.
1612 rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
1613 NS_LITERAL_CSTRING("CREATE TABLE moz_cookies ("
1614 "id INTEGER PRIMARY KEY, "
1615 "baseDomain TEXT, "
1616 "appId INTEGER DEFAULT 0, "
1617 "inBrowserElement INTEGER DEFAULT 0, "
1618 "name TEXT, "
1619 "value TEXT, "
1620 "host TEXT, "
1621 "path TEXT, "
1622 "expiry INTEGER, "
1623 "lastAccessed INTEGER, "
1624 "creationTime INTEGER, "
1625 "isSecure INTEGER, "
1626 "isHttpOnly INTEGER, "
1627 "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, "
1628 "appId, inBrowserElement)"
1629 ")"));
1630 if (NS_FAILED(rv)) return rv;
1631
1632 // Create an index on baseDomain.
1633 return mDefaultDBState->syncConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
1634 "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, "
1635 "appId, "
1636 "inBrowserElement)"));
1637 }
1638
CloseDBStates()1639 void nsCookieService::CloseDBStates() {
1640 // return if we already closed
1641 if (!mDBState) {
1642 return;
1643 }
1644
1645 if (mThread) {
1646 mThread->Shutdown();
1647 mThread = nullptr;
1648 }
1649
1650 // Null out our private and pointer DBStates regardless.
1651 mPrivateDBState = nullptr;
1652 mDBState = nullptr;
1653
1654 // If we don't have a default DBState, we're done.
1655 if (!mDefaultDBState) return;
1656
1657 // Cleanup cached statements before we can close anything.
1658 CleanupCachedStatements();
1659
1660 if (mDefaultDBState->dbConn) {
1661 // Asynchronously close the connection. We will null it below.
1662 mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener);
1663 }
1664
1665 CleanupDefaultDBConnection();
1666
1667 mDefaultDBState = nullptr;
1668 mInitializedDBConn = false;
1669 mInitializedDBStates = false;
1670 }
1671
1672 // Null out the statements.
1673 // This must be done before closing the connection.
CleanupCachedStatements()1674 void nsCookieService::CleanupCachedStatements() {
1675 mDefaultDBState->stmtInsert = nullptr;
1676 mDefaultDBState->stmtDelete = nullptr;
1677 mDefaultDBState->stmtUpdate = nullptr;
1678 }
1679
1680 // Null out the listeners, and the database connection itself. This
1681 // will not null out the statements, cancel a pending read or
1682 // asynchronously close the connection -- these must be done
1683 // beforehand if necessary.
CleanupDefaultDBConnection()1684 void nsCookieService::CleanupDefaultDBConnection() {
1685 MOZ_ASSERT(!mDefaultDBState->stmtInsert, "stmtInsert has been cleaned up");
1686 MOZ_ASSERT(!mDefaultDBState->stmtDelete, "stmtDelete has been cleaned up");
1687 MOZ_ASSERT(!mDefaultDBState->stmtUpdate, "stmtUpdate has been cleaned up");
1688
1689 // Null out the database connections. If 'dbConn' has not been used for any
1690 // asynchronous operations yet, this will synchronously close it; otherwise,
1691 // it's expected that the caller has performed an AsyncClose prior.
1692 mDefaultDBState->dbConn = nullptr;
1693
1694 // Manually null out our listeners. This is necessary because they hold a
1695 // strong ref to the DBState itself. They'll stay alive until whatever
1696 // statements are still executing complete.
1697 mDefaultDBState->insertListener = nullptr;
1698 mDefaultDBState->updateListener = nullptr;
1699 mDefaultDBState->removeListener = nullptr;
1700 mDefaultDBState->closeListener = nullptr;
1701 }
1702
HandleDBClosed(DBState * aDBState)1703 void nsCookieService::HandleDBClosed(DBState *aDBState) {
1704 COOKIE_LOGSTRING(LogLevel::Debug,
1705 ("HandleDBClosed(): DBState %p closed", aDBState));
1706
1707 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
1708
1709 switch (aDBState->corruptFlag) {
1710 case DBState::OK: {
1711 // Database is healthy. Notify of closure.
1712 if (os) {
1713 os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
1714 }
1715 break;
1716 }
1717 case DBState::CLOSING_FOR_REBUILD: {
1718 // Our close finished. Start the rebuild, and notify of db closure later.
1719 RebuildCorruptDB(aDBState);
1720 break;
1721 }
1722 case DBState::REBUILDING: {
1723 // We encountered an error during rebuild, closed the database, and now
1724 // here we are. We already have a 'cookies.sqlite.bak' from the original
1725 // dead database; we don't want to overwrite it, so let's move this one to
1726 // 'cookies.sqlite.bak-rebuild'.
1727 nsCOMPtr<nsIFile> backupFile;
1728 aDBState->cookieFile->Clone(getter_AddRefs(backupFile));
1729 nsresult rv = backupFile->MoveToNative(
1730 nullptr, NS_LITERAL_CSTRING(COOKIES_FILE ".bak-rebuild"));
1731
1732 COOKIE_LOGSTRING(LogLevel::Warning,
1733 ("HandleDBClosed(): DBState %p encountered error "
1734 "rebuilding db; move to "
1735 "'cookies.sqlite.bak-rebuild' gave rv 0x%" PRIx32,
1736 aDBState, static_cast<uint32_t>(rv)));
1737 if (os) {
1738 os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
1739 }
1740 break;
1741 }
1742 }
1743 }
1744
HandleCorruptDB(DBState * aDBState)1745 void nsCookieService::HandleCorruptDB(DBState *aDBState) {
1746 if (mDefaultDBState != aDBState) {
1747 // We've either closed the state or we've switched profiles. It's getting
1748 // a bit late to rebuild -- bail instead.
1749 COOKIE_LOGSTRING(
1750 LogLevel::Warning,
1751 ("HandleCorruptDB(): DBState %p is already closed, aborting",
1752 aDBState));
1753 return;
1754 }
1755
1756 COOKIE_LOGSTRING(LogLevel::Debug,
1757 ("HandleCorruptDB(): DBState %p has corruptFlag %u",
1758 aDBState, aDBState->corruptFlag));
1759
1760 // Mark the database corrupt, so the close listener can begin reconstructing
1761 // it.
1762 switch (mDefaultDBState->corruptFlag) {
1763 case DBState::OK: {
1764 // Move to 'closing' state.
1765 mDefaultDBState->corruptFlag = DBState::CLOSING_FOR_REBUILD;
1766
1767 CleanupCachedStatements();
1768 mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener);
1769 CleanupDefaultDBConnection();
1770 break;
1771 }
1772 case DBState::CLOSING_FOR_REBUILD: {
1773 // We had an error while waiting for close completion. That's OK, just
1774 // ignore it -- we're rebuilding anyway.
1775 return;
1776 }
1777 case DBState::REBUILDING: {
1778 // We had an error while rebuilding the DB. Game over. Close the database
1779 // and let the close handler do nothing; then we'll move it out of the
1780 // way.
1781 CleanupCachedStatements();
1782 if (mDefaultDBState->dbConn) {
1783 mDefaultDBState->dbConn->AsyncClose(mDefaultDBState->closeListener);
1784 }
1785 CleanupDefaultDBConnection();
1786 break;
1787 }
1788 }
1789 }
1790
RebuildCorruptDB(DBState * aDBState)1791 void nsCookieService::RebuildCorruptDB(DBState *aDBState) {
1792 NS_ASSERTION(!aDBState->dbConn, "shouldn't have an open db connection");
1793 NS_ASSERTION(aDBState->corruptFlag == DBState::CLOSING_FOR_REBUILD,
1794 "should be in CLOSING_FOR_REBUILD state");
1795
1796 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
1797
1798 aDBState->corruptFlag = DBState::REBUILDING;
1799
1800 if (mDefaultDBState != aDBState) {
1801 // We've either closed the state or we've switched profiles. It's getting
1802 // a bit late to rebuild -- bail instead. In any case, we were waiting
1803 // on rebuild completion to notify of the db closure, which won't happen --
1804 // do so now.
1805 COOKIE_LOGSTRING(
1806 LogLevel::Warning,
1807 ("RebuildCorruptDB(): DBState %p is stale, aborting", aDBState));
1808 if (os) {
1809 os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
1810 }
1811 return;
1812 }
1813
1814 COOKIE_LOGSTRING(LogLevel::Debug,
1815 ("RebuildCorruptDB(): creating new database"));
1816
1817 nsCOMPtr<nsIRunnable> runnable =
1818 NS_NewRunnableFunction("RebuildCorruptDB.TryInitDB", [] {
1819 NS_ENSURE_TRUE_VOID(gCookieService && gCookieService->mDefaultDBState);
1820
1821 // The database has been closed, and we're ready to rebuild. Open a
1822 // connection.
1823 OpenDBResult result = gCookieService->TryInitDB(true);
1824
1825 nsCOMPtr<nsIRunnable> innerRunnable = NS_NewRunnableFunction(
1826 "RebuildCorruptDB.TryInitDBComplete", [result] {
1827 NS_ENSURE_TRUE_VOID(gCookieService &&
1828 gCookieService->mDefaultDBState);
1829
1830 nsCOMPtr<nsIObserverService> os =
1831 mozilla::services::GetObserverService();
1832 if (result != RESULT_OK) {
1833 // We're done. Reset our DB connection and statements, and
1834 // notify of closure.
1835 COOKIE_LOGSTRING(
1836 LogLevel::Warning,
1837 ("RebuildCorruptDB(): TryInitDB() failed with result %u",
1838 result));
1839 gCookieService->CleanupCachedStatements();
1840 gCookieService->CleanupDefaultDBConnection();
1841 gCookieService->mDefaultDBState->corruptFlag = DBState::OK;
1842 if (os) {
1843 os->NotifyObservers(nullptr, "cookie-db-closed", nullptr);
1844 }
1845 return;
1846 }
1847
1848 // Notify observers that we're beginning the rebuild.
1849 if (os) {
1850 os->NotifyObservers(nullptr, "cookie-db-rebuilding", nullptr);
1851 }
1852
1853 gCookieService->InitDBConnInternal();
1854
1855 // Enumerate the hash, and add cookies to the params array.
1856 mozIStorageAsyncStatement *stmt =
1857 gCookieService->mDefaultDBState->stmtInsert;
1858 nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
1859 stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
1860 for (auto iter =
1861 gCookieService->mDefaultDBState->hostTable.Iter();
1862 !iter.Done(); iter.Next()) {
1863 nsCookieEntry *entry = iter.Get();
1864
1865 const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
1866 for (nsCookieEntry::IndexType i = 0; i < cookies.Length();
1867 ++i) {
1868 nsCookie *cookie = cookies[i];
1869
1870 if (!cookie->IsSession()) {
1871 bindCookieParameters(paramsArray, nsCookieKey(entry),
1872 cookie);
1873 }
1874 }
1875 }
1876
1877 // Make sure we've got something to write. If we don't, we're
1878 // done.
1879 uint32_t length;
1880 paramsArray->GetLength(&length);
1881 if (length == 0) {
1882 COOKIE_LOGSTRING(
1883 LogLevel::Debug,
1884 ("RebuildCorruptDB(): nothing to write, rebuild complete"));
1885 gCookieService->mDefaultDBState->corruptFlag = DBState::OK;
1886 return;
1887 }
1888
1889 // Execute the statement. If any errors crop up, we won't try
1890 // again.
1891 DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
1892 NS_ASSERT_SUCCESS(rv);
1893 nsCOMPtr<mozIStoragePendingStatement> handle;
1894 rv = stmt->ExecuteAsync(
1895 gCookieService->mDefaultDBState->insertListener,
1896 getter_AddRefs(handle));
1897 NS_ASSERT_SUCCESS(rv);
1898 });
1899 NS_DispatchToMainThread(innerRunnable);
1900 });
1901 mThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
1902 }
1903
~nsCookieService()1904 nsCookieService::~nsCookieService() {
1905 CloseDBStates();
1906
1907 UnregisterWeakMemoryReporter(this);
1908
1909 gCookieService = nullptr;
1910 }
1911
1912 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)1913 nsCookieService::Observe(nsISupports *aSubject, const char *aTopic,
1914 const char16_t *aData) {
1915 // check the topic
1916 if (!strcmp(aTopic, "profile-before-change")) {
1917 // The profile is about to change,
1918 // or is going away because the application is shutting down.
1919
1920 // Close the default DB connection and null out our DBStates before
1921 // changing.
1922 CloseDBStates();
1923
1924 } else if (!strcmp(aTopic, "profile-do-change")) {
1925 NS_ASSERTION(!mDefaultDBState, "shouldn't have a default DBState");
1926 NS_ASSERTION(!mPrivateDBState, "shouldn't have a private DBState");
1927
1928 // the profile has already changed; init the db from the new location.
1929 // if we are in the private browsing state, however, we do not want to read
1930 // data into it - we should instead put it into the default state, so it's
1931 // ready for us if and when we switch back to it.
1932 InitDBStates();
1933
1934 } else if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
1935 nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject);
1936 if (prefBranch) PrefChanged(prefBranch);
1937
1938 } else if (!strcmp(aTopic, "last-pb-context-exited")) {
1939 // Flush all the cookies stored by private browsing contexts
1940 mozilla::OriginAttributesPattern pattern;
1941 pattern.mPrivateBrowsingId.Construct(1);
1942 RemoveCookiesWithOriginAttributes(pattern, EmptyCString());
1943 mPrivateDBState = new DBState();
1944 }
1945
1946 return NS_OK;
1947 }
1948
1949 NS_IMETHODIMP
GetCookieString(nsIURI * aHostURI,nsIChannel * aChannel,char ** aCookie)1950 nsCookieService::GetCookieString(nsIURI *aHostURI, nsIChannel *aChannel,
1951 char **aCookie) {
1952 return GetCookieStringCommon(aHostURI, aChannel, false, aCookie);
1953 }
1954
1955 NS_IMETHODIMP
GetCookieStringFromHttp(nsIURI * aHostURI,nsIURI * aFirstURI,nsIChannel * aChannel,char ** aCookie)1956 nsCookieService::GetCookieStringFromHttp(nsIURI *aHostURI, nsIURI *aFirstURI,
1957 nsIChannel *aChannel, char **aCookie) {
1958 return GetCookieStringCommon(aHostURI, aChannel, true, aCookie);
1959 }
1960
GetCookieStringCommon(nsIURI * aHostURI,nsIChannel * aChannel,bool aHttpBound,char ** aCookie)1961 nsresult nsCookieService::GetCookieStringCommon(nsIURI *aHostURI,
1962 nsIChannel *aChannel,
1963 bool aHttpBound,
1964 char **aCookie) {
1965 NS_ENSURE_ARG(aHostURI);
1966 NS_ENSURE_ARG(aCookie);
1967
1968 // Determine whether the request is foreign. Failure is acceptable.
1969 bool isForeign = true;
1970 mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
1971
1972 // Get originAttributes.
1973 OriginAttributes attrs;
1974 if (aChannel) {
1975 NS_GetOriginAttributes(aChannel, attrs);
1976 }
1977
1978 bool isSafeTopLevelNav = NS_IsSafeTopLevelNav(aChannel);
1979 bool isSameSiteForeign = NS_IsSameSiteForeign(aChannel, aHostURI);
1980 nsAutoCString result;
1981 GetCookieStringInternal(aHostURI, isForeign, isSafeTopLevelNav,
1982 isSameSiteForeign, aHttpBound, attrs, result);
1983 *aCookie = result.IsEmpty() ? nullptr : ToNewCString(result);
1984 return NS_OK;
1985 }
1986
1987 NS_IMETHODIMP
SetCookieString(nsIURI * aHostURI,nsIPrompt * aPrompt,const char * aCookieHeader,nsIChannel * aChannel)1988 nsCookieService::SetCookieString(nsIURI *aHostURI, nsIPrompt *aPrompt,
1989 const char *aCookieHeader,
1990 nsIChannel *aChannel) {
1991 // The aPrompt argument is deprecated and unused. Avoid introducing new
1992 // code that uses this argument by warning if the value is non-null.
1993 MOZ_ASSERT(!aPrompt);
1994 if (aPrompt) {
1995 nsCOMPtr<nsIConsoleService> aConsoleService =
1996 do_GetService("@mozilla.org/consoleservice;1");
1997 if (aConsoleService) {
1998 aConsoleService->LogStringMessage(
1999 u"Non-null prompt ignored by nsCookieService.");
2000 }
2001 }
2002 return SetCookieStringCommon(aHostURI, aCookieHeader, nullptr, aChannel,
2003 false);
2004 }
2005
2006 NS_IMETHODIMP
SetCookieStringFromHttp(nsIURI * aHostURI,nsIURI * aFirstURI,nsIPrompt * aPrompt,const char * aCookieHeader,const char * aServerTime,nsIChannel * aChannel)2007 nsCookieService::SetCookieStringFromHttp(nsIURI *aHostURI, nsIURI *aFirstURI,
2008 nsIPrompt *aPrompt,
2009 const char *aCookieHeader,
2010 const char *aServerTime,
2011 nsIChannel *aChannel) {
2012 // The aPrompt argument is deprecated and unused. Avoid introducing new
2013 // code that uses this argument by warning if the value is non-null.
2014 MOZ_ASSERT(!aPrompt);
2015 if (aPrompt) {
2016 nsCOMPtr<nsIConsoleService> aConsoleService =
2017 do_GetService("@mozilla.org/consoleservice;1");
2018 if (aConsoleService) {
2019 aConsoleService->LogStringMessage(
2020 u"Non-null prompt ignored by nsCookieService.");
2021 }
2022 }
2023 return SetCookieStringCommon(aHostURI, aCookieHeader, aServerTime, aChannel,
2024 true);
2025 }
2026
ParseServerTime(const nsCString & aServerTime)2027 int64_t nsCookieService::ParseServerTime(const nsCString &aServerTime) {
2028 // parse server local time. this is not just done here for efficiency
2029 // reasons - if there's an error parsing it, and we need to default it
2030 // to the current time, we must do it here since the current time in
2031 // SetCookieInternal() will change for each cookie processed (e.g. if the
2032 // user is prompted).
2033 PRTime tempServerTime;
2034 int64_t serverTime;
2035 PRStatus result =
2036 PR_ParseTimeString(aServerTime.get(), true, &tempServerTime);
2037 if (result == PR_SUCCESS) {
2038 serverTime = tempServerTime / int64_t(PR_USEC_PER_SEC);
2039 } else {
2040 serverTime = PR_Now() / PR_USEC_PER_SEC;
2041 }
2042
2043 return serverTime;
2044 }
2045
SetCookieStringCommon(nsIURI * aHostURI,const char * aCookieHeader,const char * aServerTime,nsIChannel * aChannel,bool aFromHttp)2046 nsresult nsCookieService::SetCookieStringCommon(nsIURI *aHostURI,
2047 const char *aCookieHeader,
2048 const char *aServerTime,
2049 nsIChannel *aChannel,
2050 bool aFromHttp) {
2051 NS_ENSURE_ARG(aHostURI);
2052 NS_ENSURE_ARG(aCookieHeader);
2053
2054 // Determine whether the request is foreign. Failure is acceptable.
2055 bool isForeign = true;
2056 mThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isForeign);
2057
2058 // Get originAttributes.
2059 OriginAttributes attrs;
2060 if (aChannel) {
2061 NS_GetOriginAttributes(aChannel, attrs);
2062 }
2063
2064 nsDependentCString cookieString(aCookieHeader);
2065 nsDependentCString serverTime(aServerTime ? aServerTime : "");
2066 SetCookieStringInternal(aHostURI, isForeign, cookieString, serverTime,
2067 aFromHttp, attrs, aChannel);
2068 return NS_OK;
2069 }
2070
SetCookieStringInternal(nsIURI * aHostURI,bool aIsForeign,nsDependentCString & aCookieHeader,const nsCString & aServerTime,bool aFromHttp,const OriginAttributes & aOriginAttrs,nsIChannel * aChannel)2071 void nsCookieService::SetCookieStringInternal(
2072 nsIURI *aHostURI, bool aIsForeign, nsDependentCString &aCookieHeader,
2073 const nsCString &aServerTime, bool aFromHttp,
2074 const OriginAttributes &aOriginAttrs, nsIChannel *aChannel) {
2075 NS_ASSERTION(aHostURI, "null host!");
2076
2077 if (!mDBState) {
2078 NS_WARNING("No DBState! Profile already closed?");
2079 return;
2080 }
2081
2082 EnsureReadComplete(true);
2083
2084 AutoRestore<DBState *> savePrevDBState(mDBState);
2085 mDBState =
2086 (aOriginAttrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState;
2087
2088 // get the base domain for the host URI.
2089 // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk".
2090 // file:// URI's (i.e. with an empty host) are allowed, but any other
2091 // scheme must have a non-empty host. A trailing dot in the host
2092 // is acceptable.
2093 bool requireHostMatch;
2094 nsAutoCString baseDomain;
2095 nsresult rv =
2096 GetBaseDomain(mTLDService, aHostURI, baseDomain, requireHostMatch);
2097 if (NS_FAILED(rv)) {
2098 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
2099 "couldn't get base domain from URI");
2100 return;
2101 }
2102
2103 nsCookieKey key(baseDomain, aOriginAttrs);
2104
2105 // check default prefs
2106 uint32_t priorCookieCount = 0;
2107 nsAutoCString hostFromURI;
2108 aHostURI->GetHost(hostFromURI);
2109 CountCookiesFromHost(hostFromURI, &priorCookieCount);
2110 CookieStatus cookieStatus =
2111 CheckPrefs(mPermissionService, mCookieBehavior, mThirdPartySession,
2112 mThirdPartyNonsecureSession, aHostURI, aIsForeign,
2113 aCookieHeader.get(), priorCookieCount, aOriginAttrs);
2114
2115 // fire a notification if third party or if cookie was rejected
2116 // (but not if there was an error)
2117 switch (cookieStatus) {
2118 case STATUS_REJECTED:
2119 NotifyRejected(aHostURI);
2120 if (aIsForeign) {
2121 NotifyThirdParty(aHostURI, false, aChannel);
2122 }
2123 return; // Stop here
2124 case STATUS_REJECTED_WITH_ERROR:
2125 return;
2126 case STATUS_ACCEPTED: // Fallthrough
2127 case STATUS_ACCEPT_SESSION:
2128 if (aIsForeign) {
2129 NotifyThirdParty(aHostURI, true, aChannel);
2130 }
2131 break;
2132 default:
2133 break;
2134 }
2135
2136 int64_t serverTime = ParseServerTime(aServerTime);
2137
2138 // process each cookie in the header
2139 while (SetCookieInternal(aHostURI, key, requireHostMatch, cookieStatus,
2140 aCookieHeader, serverTime, aFromHttp, aChannel)) {
2141 // document.cookie can only set one cookie at a time
2142 if (!aFromHttp) break;
2143 }
2144 }
2145
2146 // notify observers that a cookie was rejected due to the users' prefs.
NotifyRejected(nsIURI * aHostURI)2147 void nsCookieService::NotifyRejected(nsIURI *aHostURI) {
2148 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
2149 if (os) {
2150 os->NotifyObservers(aHostURI, "cookie-rejected", nullptr);
2151 }
2152 }
2153
2154 // notify observers that a third-party cookie was accepted/rejected
2155 // if the cookie issuer is unknown, it defaults to "?"
NotifyThirdParty(nsIURI * aHostURI,bool aIsAccepted,nsIChannel * aChannel)2156 void nsCookieService::NotifyThirdParty(nsIURI *aHostURI, bool aIsAccepted,
2157 nsIChannel *aChannel) {
2158 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
2159 if (!os) {
2160 return;
2161 }
2162
2163 const char *topic;
2164
2165 if (mDBState != mPrivateDBState) {
2166 // Regular (non-private) browsing
2167 if (aIsAccepted) {
2168 topic = "third-party-cookie-accepted";
2169 } else {
2170 topic = "third-party-cookie-rejected";
2171 }
2172 } else {
2173 // Private browsing
2174 if (aIsAccepted) {
2175 topic = "private-third-party-cookie-accepted";
2176 } else {
2177 topic = "private-third-party-cookie-rejected";
2178 }
2179 }
2180
2181 do {
2182 // Attempt to find the host of aChannel.
2183 if (!aChannel) {
2184 break;
2185 }
2186 nsCOMPtr<nsIURI> channelURI;
2187 nsresult rv = aChannel->GetURI(getter_AddRefs(channelURI));
2188 if (NS_FAILED(rv)) {
2189 break;
2190 }
2191
2192 nsAutoCString referringHost;
2193 rv = channelURI->GetHost(referringHost);
2194 if (NS_FAILED(rv)) {
2195 break;
2196 }
2197
2198 nsAutoString referringHostUTF16 = NS_ConvertUTF8toUTF16(referringHost);
2199 os->NotifyObservers(aHostURI, topic, referringHostUTF16.get());
2200 return;
2201 } while (false);
2202
2203 // This can fail for a number of reasons, in which kind we fallback to "?"
2204 os->NotifyObservers(aHostURI, topic, u"?");
2205 }
2206
2207 // notify observers that the cookie list changed. there are five possible
2208 // values for aData:
2209 // "deleted" means a cookie was deleted. aSubject is the deleted cookie.
2210 // "added" means a cookie was added. aSubject is the added cookie.
2211 // "changed" means a cookie was altered. aSubject is the new cookie.
2212 // "cleared" means the entire cookie list was cleared. aSubject is null.
2213 // "batch-deleted" means a set of cookies was purged. aSubject is the list of
2214 // cookies.
NotifyChanged(nsISupports * aSubject,const char16_t * aData,bool aOldCookieIsSession,bool aFromHttp)2215 void nsCookieService::NotifyChanged(nsISupports *aSubject,
2216 const char16_t *aData,
2217 bool aOldCookieIsSession, bool aFromHttp) {
2218 const char *topic =
2219 mDBState == mPrivateDBState ? "private-cookie-changed" : "cookie-changed";
2220 nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
2221 if (!os) {
2222 return;
2223 }
2224 // Notify for topic "private-cookie-changed" or "cookie-changed"
2225 os->NotifyObservers(aSubject, topic, aData);
2226
2227 // Notify for topic "session-cookie-changed" to update the copy of session
2228 // cookies in session restore component.
2229 // Ignore private session cookies since they will not be restored.
2230 if (mDBState == mPrivateDBState) {
2231 return;
2232 }
2233 // Filter out notifications for individual non-session cookies.
2234 if (NS_LITERAL_STRING("changed").Equals(aData) ||
2235 NS_LITERAL_STRING("deleted").Equals(aData) ||
2236 NS_LITERAL_STRING("added").Equals(aData)) {
2237 nsCOMPtr<nsICookie> xpcCookie = do_QueryInterface(aSubject);
2238 MOZ_ASSERT(xpcCookie);
2239 auto cookie = static_cast<nsCookie *>(xpcCookie.get());
2240 if (!cookie->IsSession() && !aOldCookieIsSession) {
2241 return;
2242 }
2243 }
2244 os->NotifyObservers(aSubject, "session-cookie-changed", aData);
2245 }
2246
CreatePurgeList(nsICookie2 * aCookie)2247 already_AddRefed<nsIArray> nsCookieService::CreatePurgeList(
2248 nsICookie2 *aCookie) {
2249 nsCOMPtr<nsIMutableArray> removedList =
2250 do_CreateInstance(NS_ARRAY_CONTRACTID);
2251 removedList->AppendElement(aCookie);
2252 return removedList.forget();
2253 }
2254
2255 /******************************************************************************
2256 * nsCookieService:
2257 * public transaction helper impl
2258 ******************************************************************************/
2259
2260 NS_IMETHODIMP
RunInTransaction(nsICookieTransactionCallback * aCallback)2261 nsCookieService::RunInTransaction(nsICookieTransactionCallback *aCallback) {
2262 NS_ENSURE_ARG(aCallback);
2263 if (!mDBState) {
2264 NS_WARNING("No DBState! Profile already closed?");
2265 return NS_ERROR_NOT_AVAILABLE;
2266 }
2267
2268 EnsureReadComplete(true);
2269
2270 if (NS_WARN_IF(!mDefaultDBState->dbConn)) {
2271 return NS_ERROR_NOT_AVAILABLE;
2272 }
2273 mozStorageTransaction transaction(mDefaultDBState->dbConn, true);
2274
2275 if (NS_FAILED(aCallback->Callback())) {
2276 Unused << transaction.Rollback();
2277 return NS_ERROR_FAILURE;
2278 }
2279 return NS_OK;
2280 }
2281
2282 /******************************************************************************
2283 * nsCookieService:
2284 * pref observer impl
2285 ******************************************************************************/
2286
PrefChanged(nsIPrefBranch * aPrefBranch)2287 void nsCookieService::PrefChanged(nsIPrefBranch *aPrefBranch) {
2288 int32_t val;
2289 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookieBehavior, &val)))
2290 mCookieBehavior = (uint8_t)LIMIT(val, 0, 3, 0);
2291
2292 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxNumberOfCookies, &val)))
2293 mMaxNumberOfCookies = (uint16_t)LIMIT(val, 1, 0xFFFF, kMaxNumberOfCookies);
2294
2295 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefMaxCookiesPerHost, &val)))
2296 mMaxCookiesPerHost = (uint16_t)LIMIT(val, 1, 0xFFFF, kMaxCookiesPerHost);
2297
2298 if (NS_SUCCEEDED(aPrefBranch->GetIntPref(kPrefCookiePurgeAge, &val))) {
2299 mCookiePurgeAge =
2300 int64_t(LIMIT(val, 0, INT32_MAX, INT32_MAX)) * PR_USEC_PER_SEC;
2301 }
2302
2303 bool boolval;
2304 if (NS_SUCCEEDED(aPrefBranch->GetBoolPref(kPrefThirdPartySession, &boolval)))
2305 mThirdPartySession = boolval;
2306
2307 if (NS_SUCCEEDED(
2308 aPrefBranch->GetBoolPref(kPrefThirdPartyNonsecureSession, &boolval)))
2309 mThirdPartyNonsecureSession = boolval;
2310
2311 if (NS_SUCCEEDED(
2312 aPrefBranch->GetBoolPref(kCookieLeaveSecurityAlone, &boolval)))
2313 mLeaveSecureAlone = boolval;
2314 }
2315
2316 /******************************************************************************
2317 * nsICookieManager impl:
2318 * nsICookieManager
2319 ******************************************************************************/
2320
2321 NS_IMETHODIMP
RemoveAll()2322 nsCookieService::RemoveAll() {
2323 if (!mDBState) {
2324 NS_WARNING("No DBState! Profile already closed?");
2325 return NS_ERROR_NOT_AVAILABLE;
2326 }
2327
2328 EnsureReadComplete(true);
2329
2330 RemoveAllFromMemory();
2331
2332 // clear the cookie file
2333 if (mDBState->dbConn) {
2334 NS_ASSERTION(mDBState == mDefaultDBState, "not in default DB state");
2335
2336 nsCOMPtr<mozIStorageAsyncStatement> stmt;
2337 nsresult rv = mDefaultDBState->dbConn->CreateAsyncStatement(
2338 NS_LITERAL_CSTRING("DELETE FROM moz_cookies"), getter_AddRefs(stmt));
2339 if (NS_SUCCEEDED(rv)) {
2340 nsCOMPtr<mozIStoragePendingStatement> handle;
2341 rv = stmt->ExecuteAsync(mDefaultDBState->removeListener,
2342 getter_AddRefs(handle));
2343 NS_ASSERT_SUCCESS(rv);
2344 } else {
2345 // Recreate the database.
2346 COOKIE_LOGSTRING(LogLevel::Debug,
2347 ("RemoveAll(): corruption detected with rv 0x%" PRIx32,
2348 static_cast<uint32_t>(rv)));
2349 HandleCorruptDB(mDefaultDBState);
2350 }
2351 }
2352
2353 NotifyChanged(nullptr, u"cleared");
2354 return NS_OK;
2355 }
2356
2357 NS_IMETHODIMP
GetEnumerator(nsISimpleEnumerator ** aEnumerator)2358 nsCookieService::GetEnumerator(nsISimpleEnumerator **aEnumerator) {
2359 if (!mDBState) {
2360 NS_WARNING("No DBState! Profile already closed?");
2361 return NS_ERROR_NOT_AVAILABLE;
2362 }
2363
2364 EnsureReadComplete(true);
2365
2366 nsCOMArray<nsICookie> cookieList(mDBState->cookieCount);
2367 for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
2368 const nsCookieEntry::ArrayType &cookies = iter.Get()->GetCookies();
2369 for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
2370 cookieList.AppendObject(cookies[i]);
2371 }
2372 }
2373
2374 return NS_NewArrayEnumerator(aEnumerator, cookieList);
2375 }
2376
2377 NS_IMETHODIMP
GetSessionEnumerator(nsISimpleEnumerator ** aEnumerator)2378 nsCookieService::GetSessionEnumerator(nsISimpleEnumerator **aEnumerator) {
2379 if (!mDBState) {
2380 NS_WARNING("No DBState! Profile already closed?");
2381 return NS_ERROR_NOT_AVAILABLE;
2382 }
2383
2384 EnsureReadComplete(true);
2385
2386 nsCOMArray<nsICookie> cookieList(mDBState->cookieCount);
2387 for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
2388 const nsCookieEntry::ArrayType &cookies = iter.Get()->GetCookies();
2389 for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
2390 nsCookie *cookie = cookies[i];
2391 // Filter out non-session cookies.
2392 if (cookie->IsSession()) {
2393 cookieList.AppendObject(cookie);
2394 }
2395 }
2396 }
2397
2398 return NS_NewArrayEnumerator(aEnumerator, cookieList);
2399 }
2400
InitializeOriginAttributes(OriginAttributes * aAttrs,JS::HandleValue aOriginAttributes,JSContext * aCx,uint8_t aArgc,const char16_t * aAPI,const char16_t * aInterfaceSuffix)2401 static nsresult InitializeOriginAttributes(OriginAttributes *aAttrs,
2402 JS::HandleValue aOriginAttributes,
2403 JSContext *aCx, uint8_t aArgc,
2404 const char16_t *aAPI,
2405 const char16_t *aInterfaceSuffix) {
2406 MOZ_ASSERT(aAttrs);
2407 MOZ_ASSERT(aCx);
2408 MOZ_ASSERT(aAPI);
2409 MOZ_ASSERT(aInterfaceSuffix);
2410
2411 if (aArgc == 0) {
2412 const char16_t *params[] = {aAPI, aInterfaceSuffix};
2413
2414 // This is supposed to be temporary and in 1 or 2 releases we want to
2415 // have originAttributes param as mandatory. But for now, we don't want to
2416 // break existing addons, so we write a console message to inform the addon
2417 // developers about it.
2418 nsContentUtils::ReportToConsole(
2419 nsIScriptError::warningFlag, NS_LITERAL_CSTRING("Cookie Manager"),
2420 nullptr, nsContentUtils::eNECKO_PROPERTIES,
2421 "nsICookieManagerAPIDeprecated", params, ArrayLength(params));
2422 } else if (aArgc == 1) {
2423 if (!aOriginAttributes.isObject() ||
2424 !aAttrs->Init(aCx, aOriginAttributes)) {
2425 return NS_ERROR_INVALID_ARG;
2426 }
2427 }
2428
2429 return NS_OK;
2430 }
2431
2432 NS_IMETHODIMP
Add(const nsACString & aHost,const nsACString & aPath,const nsACString & aName,const nsACString & aValue,bool aIsSecure,bool aIsHttpOnly,bool aIsSession,int64_t aExpiry,JS::HandleValue aOriginAttributes,int32_t aSameSite,JSContext * aCx,uint8_t aArgc)2433 nsCookieService::Add(const nsACString &aHost, const nsACString &aPath,
2434 const nsACString &aName, const nsACString &aValue,
2435 bool aIsSecure, bool aIsHttpOnly, bool aIsSession,
2436 int64_t aExpiry, JS::HandleValue aOriginAttributes,
2437 int32_t aSameSite, JSContext *aCx, uint8_t aArgc) {
2438 MOZ_ASSERT(aArgc == 0 || aArgc == 1);
2439
2440 OriginAttributes attrs;
2441 nsresult rv = InitializeOriginAttributes(
2442 &attrs, aOriginAttributes, aCx, aArgc, u"nsICookieManager.add()", u"2");
2443 NS_ENSURE_SUCCESS(rv, rv);
2444
2445 return AddNative(aHost, aPath, aName, aValue, aIsSecure, aIsHttpOnly,
2446 aIsSession, aExpiry, &attrs, aSameSite);
2447 }
2448
NS_IMETHODIMP_(nsresult)2449 NS_IMETHODIMP_(nsresult)
2450 nsCookieService::AddNative(const nsACString &aHost, const nsACString &aPath,
2451 const nsACString &aName, const nsACString &aValue,
2452 bool aIsSecure, bool aIsHttpOnly, bool aIsSession,
2453 int64_t aExpiry, OriginAttributes *aOriginAttributes,
2454 int32_t aSameSite) {
2455 if (NS_WARN_IF(!aOriginAttributes)) {
2456 return NS_ERROR_FAILURE;
2457 }
2458
2459 if (!mDBState) {
2460 NS_WARNING("No DBState! Profile already closed?");
2461 return NS_ERROR_NOT_AVAILABLE;
2462 }
2463
2464 EnsureReadComplete(true);
2465
2466 AutoRestore<DBState *> savePrevDBState(mDBState);
2467 mDBState = (aOriginAttributes->mPrivateBrowsingId > 0) ? mPrivateDBState
2468 : mDefaultDBState;
2469
2470 // first, normalize the hostname, and fail if it contains illegal characters.
2471 nsAutoCString host(aHost);
2472 nsresult rv = NormalizeHost(host);
2473 NS_ENSURE_SUCCESS(rv, rv);
2474
2475 // get the base domain for the host URI.
2476 // e.g. for "www.bbc.co.uk", this would be "bbc.co.uk".
2477 nsAutoCString baseDomain;
2478 rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
2479 NS_ENSURE_SUCCESS(rv, rv);
2480
2481 int64_t currentTimeInUsec = PR_Now();
2482 nsCookieKey key = nsCookieKey(baseDomain, *aOriginAttributes);
2483
2484 RefPtr<nsCookie> cookie = nsCookie::Create(
2485 aName, aValue, host, aPath, aExpiry, currentTimeInUsec,
2486 nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), aIsSession,
2487 aIsSecure, aIsHttpOnly, key.mOriginAttributes, aSameSite);
2488 if (!cookie) {
2489 return NS_ERROR_OUT_OF_MEMORY;
2490 }
2491
2492 AddInternal(key, cookie, currentTimeInUsec, nullptr, nullptr, true);
2493 return NS_OK;
2494 }
2495
Remove(const nsACString & aHost,const OriginAttributes & aAttrs,const nsACString & aName,const nsACString & aPath,bool aBlocked)2496 nsresult nsCookieService::Remove(const nsACString &aHost,
2497 const OriginAttributes &aAttrs,
2498 const nsACString &aName,
2499 const nsACString &aPath, bool aBlocked) {
2500 if (!mDBState) {
2501 NS_WARNING("No DBState! Profile already closed?");
2502 return NS_ERROR_NOT_AVAILABLE;
2503 }
2504
2505 EnsureReadComplete(true);
2506
2507 AutoRestore<DBState *> savePrevDBState(mDBState);
2508 mDBState =
2509 (aAttrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState;
2510
2511 // first, normalize the hostname, and fail if it contains illegal characters.
2512 nsAutoCString host(aHost);
2513 nsresult rv = NormalizeHost(host);
2514 NS_ENSURE_SUCCESS(rv, rv);
2515
2516 nsAutoCString baseDomain;
2517 rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
2518 NS_ENSURE_SUCCESS(rv, rv);
2519
2520 nsListIter matchIter;
2521 RefPtr<nsCookie> cookie;
2522 if (FindCookie(nsCookieKey(baseDomain, aAttrs), host,
2523 PromiseFlatCString(aName), PromiseFlatCString(aPath),
2524 matchIter)) {
2525 cookie = matchIter.Cookie();
2526 RemoveCookieFromList(matchIter);
2527 }
2528
2529 // check if we need to add the host to the permissions blacklist.
2530 if (aBlocked && mPermissionService) {
2531 // strip off the domain dot, if necessary
2532 if (!host.IsEmpty() && host.First() == '.') host.Cut(0, 1);
2533
2534 host.InsertLiteral("http://", 0);
2535
2536 nsCOMPtr<nsIURI> uri;
2537 NS_NewURI(getter_AddRefs(uri), host);
2538
2539 if (uri)
2540 mPermissionService->SetAccess(uri, nsICookiePermission::ACCESS_DENY);
2541 }
2542
2543 if (cookie) {
2544 // Everything's done. Notify observers.
2545 NotifyChanged(cookie, u"deleted");
2546 }
2547
2548 return NS_OK;
2549 }
2550
2551 NS_IMETHODIMP
Remove(const nsACString & aHost,const nsACString & aName,const nsACString & aPath,bool aBlocked,JS::HandleValue aOriginAttributes,JSContext * aCx,uint8_t aArgc)2552 nsCookieService::Remove(const nsACString &aHost, const nsACString &aName,
2553 const nsACString &aPath, bool aBlocked,
2554 JS::HandleValue aOriginAttributes, JSContext *aCx,
2555 uint8_t aArgc) {
2556 MOZ_ASSERT(aArgc == 0 || aArgc == 1);
2557
2558 OriginAttributes attrs;
2559 nsresult rv = InitializeOriginAttributes(
2560 &attrs, aOriginAttributes, aCx, aArgc, u"nsICookieManager.remove()", u"");
2561 NS_ENSURE_SUCCESS(rv, rv);
2562
2563 return RemoveNative(aHost, aName, aPath, aBlocked, &attrs);
2564 }
2565
NS_IMETHODIMP_(nsresult)2566 NS_IMETHODIMP_(nsresult)
2567 nsCookieService::RemoveNative(const nsACString &aHost, const nsACString &aName,
2568 const nsACString &aPath, bool aBlocked,
2569 OriginAttributes *aOriginAttributes) {
2570 if (NS_WARN_IF(!aOriginAttributes)) {
2571 return NS_ERROR_FAILURE;
2572 }
2573
2574 nsresult rv = Remove(aHost, *aOriginAttributes, aName, aPath, aBlocked);
2575 if (NS_WARN_IF(NS_FAILED(rv))) {
2576 return rv;
2577 }
2578
2579 return NS_OK;
2580 }
2581
2582 /******************************************************************************
2583 * nsCookieService impl:
2584 * private file I/O functions
2585 ******************************************************************************/
2586
2587 // Extract data from a single result row and create an nsCookie.
GetCookieFromRow(mozIStorageStatement * aRow,const OriginAttributes & aOriginAttributes)2588 mozilla::UniquePtr<ConstCookie> nsCookieService::GetCookieFromRow(
2589 mozIStorageStatement *aRow, const OriginAttributes &aOriginAttributes) {
2590 // Skip reading 'baseDomain' -- up to the caller.
2591 nsCString name, value, host, path;
2592 DebugOnly<nsresult> rv = aRow->GetUTF8String(IDX_NAME, name);
2593 NS_ASSERT_SUCCESS(rv);
2594 rv = aRow->GetUTF8String(IDX_VALUE, value);
2595 NS_ASSERT_SUCCESS(rv);
2596 rv = aRow->GetUTF8String(IDX_HOST, host);
2597 NS_ASSERT_SUCCESS(rv);
2598 rv = aRow->GetUTF8String(IDX_PATH, path);
2599 NS_ASSERT_SUCCESS(rv);
2600
2601 int64_t expiry = aRow->AsInt64(IDX_EXPIRY);
2602 int64_t lastAccessed = aRow->AsInt64(IDX_LAST_ACCESSED);
2603 int64_t creationTime = aRow->AsInt64(IDX_CREATION_TIME);
2604 bool isSecure = 0 != aRow->AsInt32(IDX_SECURE);
2605 bool isHttpOnly = 0 != aRow->AsInt32(IDX_HTTPONLY);
2606 int32_t sameSite = aRow->AsInt32(IDX_SAME_SITE);
2607
2608 // Create a new constCookie and assign the data.
2609 return mozilla::MakeUnique<ConstCookie>(
2610 name, value, host, path, expiry, lastAccessed, creationTime, isSecure,
2611 isHttpOnly, aOriginAttributes, sameSite);
2612 }
2613
EnsureReadComplete(bool aInitDBConn)2614 void nsCookieService::EnsureReadComplete(bool aInitDBConn) {
2615 MOZ_ASSERT(NS_IsMainThread());
2616
2617 bool isAccumulated = false;
2618
2619 if (!mInitializedDBStates) {
2620 TimeStamp startBlockTime = TimeStamp::Now();
2621 MonitorAutoLock lock(mMonitor);
2622
2623 while (!mInitializedDBStates) {
2624 mMonitor.Wait();
2625 }
2626 Telemetry::AccumulateTimeDelta(
2627 Telemetry::MOZ_SQLITE_COOKIES_BLOCK_MAIN_THREAD_MS_V2, startBlockTime);
2628 Telemetry::Accumulate(
2629 Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0);
2630 isAccumulated = true;
2631 } else if (!mEndInitDBConn.IsNull()) {
2632 // We didn't block main thread, and here comes the first cookie request.
2633 // Collect how close we're going to block main thread.
2634 Telemetry::Accumulate(
2635 Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS,
2636 (TimeStamp::Now() - mEndInitDBConn).ToMilliseconds());
2637 // Nullify the timestamp so wo don't accumulate this telemetry probe again.
2638 mEndInitDBConn = TimeStamp();
2639 isAccumulated = true;
2640 } else if (!mInitializedDBConn && aInitDBConn) {
2641 // A request comes while we finished cookie thread task and InitDBConn is
2642 // on the way from cookie thread to main thread. We're very close to block
2643 // main thread.
2644 Telemetry::Accumulate(
2645 Telemetry::MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS, 0);
2646 isAccumulated = true;
2647 }
2648
2649 if (!mInitializedDBConn && aInitDBConn && mDefaultDBState) {
2650 InitDBConn();
2651 if (isAccumulated) {
2652 // Nullify the timestamp so wo don't accumulate this telemetry probe
2653 // again.
2654 mEndInitDBConn = TimeStamp();
2655 }
2656 }
2657 }
2658
Read()2659 OpenDBResult nsCookieService::Read() {
2660 MOZ_ASSERT(NS_GetCurrentThread() == mThread);
2661
2662 // Set up a statement to delete any rows with a nullptr 'baseDomain'
2663 // column. This takes care of any cookies set by browsers that don't
2664 // understand the 'baseDomain' column, where the database schema version
2665 // is from one that does. (This would occur when downgrading.)
2666 nsresult rv = mDefaultDBState->syncConn->ExecuteSimpleSQL(
2667 NS_LITERAL_CSTRING("DELETE FROM moz_cookies WHERE baseDomain ISNULL"));
2668 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
2669
2670 // Read in the data synchronously.
2671 // see IDX_NAME, etc. for parameter indexes
2672 nsCOMPtr<mozIStorageStatement> stmt;
2673 rv = mDefaultDBState->syncConn->CreateStatement(
2674 NS_LITERAL_CSTRING("SELECT "
2675 "name, "
2676 "value, "
2677 "host, "
2678 "path, "
2679 "expiry, "
2680 "lastAccessed, "
2681 "creationTime, "
2682 "isSecure, "
2683 "isHttpOnly, "
2684 "baseDomain, "
2685 "originAttributes, "
2686 "sameSite "
2687 "FROM moz_cookies "
2688 "WHERE baseDomain NOTNULL"),
2689 getter_AddRefs(stmt));
2690
2691 NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
2692
2693 if (NS_WARN_IF(!mReadArray.IsEmpty())) {
2694 mReadArray.Clear();
2695 }
2696 mReadArray.SetCapacity(kMaxNumberOfCookies);
2697
2698 nsCString baseDomain, name, value, host, path;
2699 bool hasResult;
2700 while (true) {
2701 rv = stmt->ExecuteStep(&hasResult);
2702 if (NS_WARN_IF(NS_FAILED(rv))) {
2703 mReadArray.Clear();
2704 return RESULT_RETRY;
2705 }
2706
2707 if (!hasResult) break;
2708
2709 // Make sure we haven't already read the data.
2710 stmt->GetUTF8String(IDX_BASE_DOMAIN, baseDomain);
2711
2712 nsAutoCString suffix;
2713 OriginAttributes attrs;
2714 stmt->GetUTF8String(IDX_ORIGIN_ATTRIBUTES, suffix);
2715 // If PopulateFromSuffix failed we just ignore the OA attributes
2716 // that we don't support
2717 Unused << attrs.PopulateFromSuffix(suffix);
2718
2719 nsCookieKey key(baseDomain, attrs);
2720 CookieDomainTuple *tuple = mReadArray.AppendElement();
2721 tuple->key = key;
2722 tuple->cookie = GetCookieFromRow(stmt, attrs);
2723 }
2724
2725 COOKIE_LOGSTRING(LogLevel::Debug,
2726 ("Read(): %zu cookies read", mReadArray.Length()));
2727
2728 return RESULT_OK;
2729 }
2730
2731 NS_IMETHODIMP
ImportCookies(nsIFile * aCookieFile)2732 nsCookieService::ImportCookies(nsIFile *aCookieFile) {
2733 if (!mDBState) {
2734 NS_WARNING("No DBState! Profile already closed?");
2735 return NS_ERROR_NOT_AVAILABLE;
2736 }
2737
2738 EnsureReadComplete(true);
2739
2740 // Make sure we're in the default DB state. We don't want people importing
2741 // cookies into a private browsing session!
2742 if (mDBState != mDefaultDBState) {
2743 NS_WARNING("Trying to import cookies in a private browsing session!");
2744 return NS_ERROR_NOT_AVAILABLE;
2745 }
2746
2747 nsresult rv;
2748 nsCOMPtr<nsIInputStream> fileInputStream;
2749 rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), aCookieFile);
2750 if (NS_FAILED(rv)) return rv;
2751
2752 nsCOMPtr<nsILineInputStream> lineInputStream =
2753 do_QueryInterface(fileInputStream, &rv);
2754 if (NS_FAILED(rv)) return rv;
2755
2756 static const char kTrue[] = "TRUE";
2757
2758 nsAutoCString buffer, baseDomain;
2759 bool isMore = true;
2760 int32_t hostIndex, isDomainIndex, pathIndex, secureIndex, expiresIndex,
2761 nameIndex, cookieIndex;
2762 nsACString::char_iterator iter;
2763 int32_t numInts;
2764 int64_t expires;
2765 bool isDomain, isHttpOnly = false;
2766 uint32_t originalCookieCount = mDefaultDBState->cookieCount;
2767
2768 int64_t currentTimeInUsec = PR_Now();
2769 int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
2770 // we use lastAccessedCounter to keep cookies in recently-used order,
2771 // so we start by initializing to currentTime (somewhat arbitrary)
2772 int64_t lastAccessedCounter = currentTimeInUsec;
2773
2774 /* file format is:
2775 *
2776 * host \t isDomain \t path \t secure \t expires \t name \t cookie
2777 *
2778 * if this format isn't respected we move onto the next line in the file.
2779 * isDomain is "TRUE" or "FALSE" (default to "FALSE")
2780 * isSecure is "TRUE" or "FALSE" (default to "TRUE")
2781 * expires is a int64_t integer
2782 * note 1: cookie can contain tabs.
2783 * note 2: cookies will be stored in order of lastAccessed time:
2784 * most-recently used come first; least-recently-used come last.
2785 */
2786
2787 /*
2788 * ...but due to bug 178933, we hide HttpOnly cookies from older code
2789 * in a comment, so they don't expose HttpOnly cookies to JS.
2790 *
2791 * The format for HttpOnly cookies is
2792 *
2793 * #HttpOnly_host \t isDomain \t path \t secure \t expires \t name \t cookie
2794 *
2795 */
2796
2797 // We will likely be adding a bunch of cookies to the DB, so we use async
2798 // batching with storage to make this super fast.
2799 nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
2800 if (originalCookieCount == 0 && mDefaultDBState->dbConn) {
2801 mDefaultDBState->stmtInsert->NewBindingParamsArray(
2802 getter_AddRefs(paramsArray));
2803 }
2804
2805 while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) {
2806 if (StringBeginsWith(buffer, NS_LITERAL_CSTRING(HTTP_ONLY_PREFIX))) {
2807 isHttpOnly = true;
2808 hostIndex = sizeof(HTTP_ONLY_PREFIX) - 1;
2809 } else if (buffer.IsEmpty() || buffer.First() == '#') {
2810 continue;
2811 } else {
2812 isHttpOnly = false;
2813 hostIndex = 0;
2814 }
2815
2816 // this is a cheap, cheesy way of parsing a tab-delimited line into
2817 // string indexes, which can be lopped off into substrings. just for
2818 // purposes of obfuscation, it also checks that each token was found.
2819 // todo: use iterators?
2820 if ((isDomainIndex = buffer.FindChar('\t', hostIndex) + 1) == 0 ||
2821 (pathIndex = buffer.FindChar('\t', isDomainIndex) + 1) == 0 ||
2822 (secureIndex = buffer.FindChar('\t', pathIndex) + 1) == 0 ||
2823 (expiresIndex = buffer.FindChar('\t', secureIndex) + 1) == 0 ||
2824 (nameIndex = buffer.FindChar('\t', expiresIndex) + 1) == 0 ||
2825 (cookieIndex = buffer.FindChar('\t', nameIndex) + 1) == 0) {
2826 continue;
2827 }
2828
2829 // check the expirytime first - if it's expired, ignore
2830 // nullstomp the trailing tab, to avoid copying the string
2831 buffer.BeginWriting(iter);
2832 *(iter += nameIndex - 1) = char(0);
2833 numInts = PR_sscanf(buffer.get() + expiresIndex, "%lld", &expires);
2834 if (numInts != 1 || expires < currentTime) {
2835 continue;
2836 }
2837
2838 isDomain = Substring(buffer, isDomainIndex, pathIndex - isDomainIndex - 1)
2839 .EqualsLiteral(kTrue);
2840 const nsACString &host =
2841 Substring(buffer, hostIndex, isDomainIndex - hostIndex - 1);
2842 // check for bad legacy cookies (domain not starting with a dot, or
2843 // containing a port), and discard
2844 if ((isDomain && !host.IsEmpty() && host.First() != '.') ||
2845 host.Contains(':')) {
2846 continue;
2847 }
2848
2849 // compute the baseDomain from the host
2850 rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
2851 if (NS_FAILED(rv)) continue;
2852
2853 // pre-existing cookies have inIsolatedMozBrowser=false set by default
2854 // constructor of OriginAttributes().
2855 nsCookieKey key = DEFAULT_APP_KEY(baseDomain);
2856
2857 // Create a new nsCookie and assign the data. We don't know the cookie
2858 // creation time, so just use the current time to generate a unique one.
2859 RefPtr<nsCookie> newCookie = nsCookie::Create(
2860 Substring(buffer, nameIndex, cookieIndex - nameIndex - 1),
2861 Substring(buffer, cookieIndex, buffer.Length() - cookieIndex), host,
2862 Substring(buffer, pathIndex, secureIndex - pathIndex - 1), expires,
2863 lastAccessedCounter,
2864 nsCookie::GenerateUniqueCreationTime(currentTimeInUsec), false,
2865 Substring(buffer, secureIndex, expiresIndex - secureIndex - 1)
2866 .EqualsLiteral(kTrue),
2867 isHttpOnly, key.mOriginAttributes, nsICookie2::SAMESITE_UNSET);
2868 if (!newCookie) {
2869 return NS_ERROR_OUT_OF_MEMORY;
2870 }
2871
2872 // trick: preserve the most-recently-used cookie ordering,
2873 // by successively decrementing the lastAccessed time
2874 lastAccessedCounter--;
2875
2876 if (originalCookieCount == 0) {
2877 AddCookieToList(key, newCookie, mDefaultDBState, paramsArray);
2878 } else {
2879 AddInternal(key, newCookie, currentTimeInUsec, nullptr, nullptr, true);
2880 }
2881 }
2882
2883 // If we need to write to disk, do so now.
2884 if (paramsArray) {
2885 uint32_t length;
2886 paramsArray->GetLength(&length);
2887 if (length) {
2888 rv = mDefaultDBState->stmtInsert->BindParameters(paramsArray);
2889 NS_ASSERT_SUCCESS(rv);
2890 nsCOMPtr<mozIStoragePendingStatement> handle;
2891 rv = mDefaultDBState->stmtInsert->ExecuteAsync(
2892 mDefaultDBState->insertListener, getter_AddRefs(handle));
2893 NS_ASSERT_SUCCESS(rv);
2894 }
2895 }
2896
2897 if (mDefaultDBState->cookieCount - originalCookieCount > 0) {
2898 Telemetry::Accumulate(Telemetry::MOZ_SQLITE_COOKIES_OLD_SCHEMA, 0);
2899 }
2900
2901 COOKIE_LOGSTRING(LogLevel::Debug,
2902 ("ImportCookies(): %" PRIu32 " cookies imported",
2903 mDefaultDBState->cookieCount));
2904
2905 return NS_OK;
2906 }
2907
2908 /******************************************************************************
2909 * nsCookieService impl:
2910 * private GetCookie/SetCookie helpers
2911 ******************************************************************************/
2912
2913 // helper function for GetCookieList
ispathdelimiter(char c)2914 static inline bool ispathdelimiter(char c) {
2915 return c == '/' || c == '?' || c == '#' || c == ';';
2916 }
2917
DomainMatches(nsCookie * aCookie,const nsACString & aHost)2918 bool nsCookieService::DomainMatches(nsCookie *aCookie,
2919 const nsACString &aHost) {
2920 // first, check for an exact host or domain cookie match, e.g. "google.com"
2921 // or ".google.com"; second a subdomain match, e.g.
2922 // host = "mail.google.com", cookie domain = ".google.com".
2923 return aCookie->RawHost() == aHost ||
2924 (aCookie->IsDomain() && StringEndsWith(aHost, aCookie->Host()));
2925 }
2926
IsSameSiteEnabled()2927 bool nsCookieService::IsSameSiteEnabled() {
2928 static bool prefInitialized = false;
2929 if (!prefInitialized) {
2930 Preferences::AddBoolVarCache(&sSameSiteEnabled,
2931 "network.cookie.same-site.enabled", false);
2932 prefInitialized = true;
2933 }
2934 return sSameSiteEnabled;
2935 }
2936
PathMatches(nsCookie * aCookie,const nsACString & aPath)2937 bool nsCookieService::PathMatches(nsCookie *aCookie, const nsACString &aPath) {
2938 // calculate cookie path length, excluding trailing '/'
2939 uint32_t cookiePathLen = aCookie->Path().Length();
2940 if (cookiePathLen > 0 && aCookie->Path().Last() == '/') --cookiePathLen;
2941
2942 // if the given path is shorter than the cookie path, it doesn't match
2943 // if the given path doesn't start with the cookie path, it doesn't match.
2944 if (!StringBeginsWith(aPath, Substring(aCookie->Path(), 0, cookiePathLen)))
2945 return false;
2946
2947 // if the given path is longer than the cookie path, and the first char after
2948 // the cookie path is not a path delimiter, it doesn't match.
2949 if (aPath.Length() > cookiePathLen &&
2950 !ispathdelimiter(aPath.CharAt(cookiePathLen))) {
2951 /*
2952 * |ispathdelimiter| tests four cases: '/', '?', '#', and ';'.
2953 * '/' is the "standard" case; the '?' test allows a site at host/abc?def
2954 * to receive a cookie that has a path attribute of abc. this seems
2955 * strange but at least one major site (citibank, bug 156725) depends
2956 * on it. The test for # and ; are put in to proactively avoid problems
2957 * with other sites - these are the only other chars allowed in the path.
2958 */
2959 return false;
2960 }
2961
2962 // either the paths match exactly, or the cookie path is a prefix of
2963 // the given path.
2964 return true;
2965 }
2966
GetCookiesForURI(nsIURI * aHostURI,bool aIsForeign,bool aIsSafeTopLevelNav,bool aIsSameSiteForeign,bool aHttpBound,const OriginAttributes & aOriginAttrs,nsTArray<nsCookie * > & aCookieList)2967 void nsCookieService::GetCookiesForURI(nsIURI *aHostURI, bool aIsForeign,
2968 bool aIsSafeTopLevelNav,
2969 bool aIsSameSiteForeign, bool aHttpBound,
2970 const OriginAttributes &aOriginAttrs,
2971 nsTArray<nsCookie *> &aCookieList) {
2972 NS_ASSERTION(aHostURI, "null host!");
2973
2974 if (!mDBState) {
2975 NS_WARNING("No DBState! Profile already closed?");
2976 return;
2977 }
2978
2979 EnsureReadComplete(true);
2980
2981 AutoRestore<DBState *> savePrevDBState(mDBState);
2982 mDBState =
2983 (aOriginAttrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState;
2984
2985 // get the base domain, host, and path from the URI.
2986 // e.g. for "www.bbc.co.uk", the base domain would be "bbc.co.uk".
2987 // file:// URI's (i.e. with an empty host) are allowed, but any other
2988 // scheme must have a non-empty host. A trailing dot in the host
2989 // is acceptable.
2990 bool requireHostMatch;
2991 nsAutoCString baseDomain, hostFromURI, pathFromURI;
2992 nsresult rv =
2993 GetBaseDomain(mTLDService, aHostURI, baseDomain, requireHostMatch);
2994 if (NS_SUCCEEDED(rv)) rv = aHostURI->GetAsciiHost(hostFromURI);
2995 if (NS_SUCCEEDED(rv)) rv = aHostURI->GetPathQueryRef(pathFromURI);
2996 if (NS_FAILED(rv)) {
2997 COOKIE_LOGFAILURE(GET_COOKIE, aHostURI, nullptr,
2998 "invalid host/path from URI");
2999 return;
3000 }
3001
3002 // check default prefs
3003 uint32_t priorCookieCount = 0;
3004 CountCookiesFromHost(hostFromURI, &priorCookieCount);
3005 CookieStatus cookieStatus =
3006 CheckPrefs(mPermissionService, mCookieBehavior, mThirdPartySession,
3007 mThirdPartyNonsecureSession, aHostURI, aIsForeign, nullptr,
3008 priorCookieCount, aOriginAttrs);
3009
3010 // for GetCookie(), we don't fire rejection notifications.
3011 switch (cookieStatus) {
3012 case STATUS_REJECTED:
3013 case STATUS_REJECTED_WITH_ERROR:
3014 return;
3015 default:
3016 break;
3017 }
3018
3019 // Note: The following permissions logic is mirrored in
3020 // extensions::MatchPattern::MatchesCookie.
3021 // If it changes, please update that function, or file a bug for someone
3022 // else to do so.
3023
3024 // check if aHostURI is using an https secure protocol.
3025 // if it isn't, then we can't send a secure cookie over the connection.
3026 // if SchemeIs fails, assume an insecure connection, to be on the safe side
3027 bool isSecure;
3028 if (NS_FAILED(aHostURI->SchemeIs("https", &isSecure))) {
3029 isSecure = false;
3030 }
3031
3032 nsCookie *cookie;
3033 int64_t currentTimeInUsec = PR_Now();
3034 int64_t currentTime = currentTimeInUsec / PR_USEC_PER_SEC;
3035 bool stale = false;
3036
3037 nsCookieKey key(baseDomain, aOriginAttrs);
3038
3039 // perform the hash lookup
3040 nsCookieEntry *entry = mDBState->hostTable.GetEntry(key);
3041 if (!entry) return;
3042
3043 // iterate the cookies!
3044 const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
3045 for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
3046 cookie = cookies[i];
3047
3048 // check the host, since the base domain lookup is conservative.
3049 if (!DomainMatches(cookie, hostFromURI)) continue;
3050
3051 // if the cookie is secure and the host scheme isn't, we can't send it
3052 if (cookie->IsSecure() && !isSecure) continue;
3053
3054 int32_t sameSiteAttr = 0;
3055 cookie->GetSameSite(&sameSiteAttr);
3056 if (aIsSameSiteForeign && IsSameSiteEnabled()) {
3057 // it if's a cross origin request and the cookie is same site only
3058 // (strict) don't send it
3059 if (sameSiteAttr == nsICookie2::SAMESITE_STRICT) {
3060 continue;
3061 }
3062 // if it's a cross origin request, the cookie is same site lax, but it's
3063 // not a top-level navigation, don't send it
3064 if (sameSiteAttr == nsICookie2::SAMESITE_LAX && !aIsSafeTopLevelNav) {
3065 continue;
3066 }
3067 }
3068
3069 // if the cookie is httpOnly and it's not going directly to the HTTP
3070 // connection, don't send it
3071 if (cookie->IsHttpOnly() && !aHttpBound) continue;
3072
3073 // if the nsIURI path doesn't match the cookie path, don't send it back
3074 if (!PathMatches(cookie, pathFromURI)) continue;
3075
3076 // check if the cookie has expired
3077 if (cookie->Expiry() <= currentTime) {
3078 continue;
3079 }
3080
3081 // all checks passed - add to list and check if lastAccessed stamp needs
3082 // updating
3083 aCookieList.AppendElement(cookie);
3084 if (cookie->IsStale()) {
3085 stale = true;
3086 }
3087 }
3088
3089 int32_t count = aCookieList.Length();
3090 if (count == 0) return;
3091
3092 // update lastAccessed timestamps. we only do this if the timestamp is stale
3093 // by a certain amount, to avoid thrashing the db during pageload.
3094 if (stale) {
3095 // Create an array of parameters to bind to our update statement. Batching
3096 // is OK here since we're updating cookies with no interleaved operations.
3097 nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
3098 mozIStorageAsyncStatement *stmt = mDBState->stmtUpdate;
3099 if (mDBState->dbConn) {
3100 stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
3101 }
3102
3103 for (int32_t i = 0; i < count; ++i) {
3104 cookie = aCookieList.ElementAt(i);
3105
3106 if (cookie->IsStale()) {
3107 UpdateCookieInList(cookie, currentTimeInUsec, paramsArray);
3108 }
3109 }
3110 // Update the database now if necessary.
3111 if (paramsArray) {
3112 uint32_t length;
3113 paramsArray->GetLength(&length);
3114 if (length) {
3115 DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
3116 NS_ASSERT_SUCCESS(rv);
3117 nsCOMPtr<mozIStoragePendingStatement> handle;
3118 rv = stmt->ExecuteAsync(mDBState->updateListener,
3119 getter_AddRefs(handle));
3120 NS_ASSERT_SUCCESS(rv);
3121 }
3122 }
3123 }
3124
3125 // return cookies in order of path length; longest to shortest.
3126 // this is required per RFC2109. if cookies match in length,
3127 // then sort by creation time (see bug 236772).
3128 aCookieList.Sort(CompareCookiesForSending());
3129 }
3130
GetCookieStringInternal(nsIURI * aHostURI,bool aIsForeign,bool aIsSafeTopLevelNav,bool aIsSameSiteForeign,bool aHttpBound,const OriginAttributes & aOriginAttrs,nsCString & aCookieString)3131 void nsCookieService::GetCookieStringInternal(
3132 nsIURI *aHostURI, bool aIsForeign, bool aIsSafeTopLevelNav,
3133 bool aIsSameSiteForeign, bool aHttpBound,
3134 const OriginAttributes &aOriginAttrs, nsCString &aCookieString) {
3135 AutoTArray<nsCookie *, 8> foundCookieList;
3136 GetCookiesForURI(aHostURI, aIsForeign, aIsSafeTopLevelNav, aIsSameSiteForeign,
3137 aHttpBound, aOriginAttrs, foundCookieList);
3138
3139 nsCookie *cookie;
3140 for (uint32_t i = 0; i < foundCookieList.Length(); ++i) {
3141 cookie = foundCookieList.ElementAt(i);
3142
3143 // check if we have anything to write
3144 if (!cookie->Name().IsEmpty() || !cookie->Value().IsEmpty()) {
3145 // if we've already added a cookie to the return list, append a "; " so
3146 // that subsequent cookies are delimited in the final list.
3147 if (!aCookieString.IsEmpty()) {
3148 aCookieString.AppendLiteral("; ");
3149 }
3150
3151 if (!cookie->Name().IsEmpty()) {
3152 // we have a name and value - write both
3153 aCookieString +=
3154 cookie->Name() + NS_LITERAL_CSTRING("=") + cookie->Value();
3155 } else {
3156 // just write value
3157 aCookieString += cookie->Value();
3158 }
3159 }
3160 }
3161
3162 if (!aCookieString.IsEmpty())
3163 COOKIE_LOGSUCCESS(GET_COOKIE, aHostURI, aCookieString, nullptr, false);
3164 }
3165
3166 // processes a single cookie, and returns true if there are more cookies
3167 // to be processed
CanSetCookie(nsIURI * aHostURI,const nsCookieKey & aKey,nsCookieAttributes & aCookieAttributes,bool aRequireHostMatch,CookieStatus aStatus,nsDependentCString & aCookieHeader,int64_t aServerTime,bool aFromHttp,nsIChannel * aChannel,bool aLeaveSecureAlone,bool & aSetCookie,mozIThirdPartyUtil * aThirdPartyUtil)3168 bool nsCookieService::CanSetCookie(nsIURI *aHostURI, const nsCookieKey &aKey,
3169 nsCookieAttributes &aCookieAttributes,
3170 bool aRequireHostMatch, CookieStatus aStatus,
3171 nsDependentCString &aCookieHeader,
3172 int64_t aServerTime, bool aFromHttp,
3173 nsIChannel *aChannel, bool aLeaveSecureAlone,
3174 bool &aSetCookie,
3175 mozIThirdPartyUtil *aThirdPartyUtil) {
3176 NS_ASSERTION(aHostURI, "null host!");
3177
3178 aSetCookie = false;
3179
3180 // init expiryTime such that session cookies won't prematurely expire
3181 aCookieAttributes.expiryTime = INT64_MAX;
3182
3183 // aCookieHeader is an in/out param to point to the next cookie, if
3184 // there is one. Save the present value for logging purposes
3185 nsDependentCString savedCookieHeader(aCookieHeader);
3186
3187 // newCookie says whether there are multiple cookies in the header;
3188 // so we can handle them separately.
3189 bool newCookie = ParseAttributes(aCookieHeader, aCookieAttributes);
3190
3191 // Collect telemetry on how often secure cookies are set from non-secure
3192 // origins, and vice-versa.
3193 //
3194 // 0 = nonsecure and "http:"
3195 // 1 = nonsecure and "https:"
3196 // 2 = secure and "http:"
3197 // 3 = secure and "https:"
3198 bool isHTTPS;
3199 nsresult rv = aHostURI->SchemeIs("https", &isHTTPS);
3200 if (NS_SUCCEEDED(rv)) {
3201 Telemetry::Accumulate(Telemetry::COOKIE_SCHEME_SECURITY,
3202 ((aCookieAttributes.isSecure) ? 0x02 : 0x00) |
3203 ((isHTTPS) ? 0x01 : 0x00));
3204
3205 // Collect telemetry on how often are first- and third-party cookies set
3206 // from HTTPS origins:
3207 //
3208 // 0 (000) = first-party and "http:"
3209 // 1 (001) = first-party and "http:" with bogus Secure cookie flag?!
3210 // 2 (010) = first-party and "https:"
3211 // 3 (011) = first-party and "https:" with Secure cookie flag
3212 // 4 (100) = third-party and "http:"
3213 // 5 (101) = third-party and "http:" with bogus Secure cookie flag?!
3214 // 6 (110) = third-party and "https:"
3215 // 7 (111) = third-party and "https:" with Secure cookie flag
3216 if (aThirdPartyUtil) {
3217 bool isThirdParty = true;
3218 aThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI, &isThirdParty);
3219 Telemetry::Accumulate(Telemetry::COOKIE_SCHEME_HTTPS,
3220 (isThirdParty ? 0x04 : 0x00) |
3221 (isHTTPS ? 0x02 : 0x00) |
3222 (aCookieAttributes.isSecure ? 0x01 : 0x00));
3223 }
3224 }
3225
3226 int64_t currentTimeInUsec = PR_Now();
3227
3228 // calculate expiry time of cookie.
3229 aCookieAttributes.isSession = GetExpiry(aCookieAttributes, aServerTime,
3230 currentTimeInUsec / PR_USEC_PER_SEC);
3231 if (aStatus == STATUS_ACCEPT_SESSION) {
3232 // force lifetime to session. note that the expiration time, if set above,
3233 // will still apply.
3234 aCookieAttributes.isSession = true;
3235 }
3236
3237 // reject cookie if it's over the size limit, per RFC2109
3238 if ((aCookieAttributes.name.Length() + aCookieAttributes.value.Length()) >
3239 kMaxBytesPerCookie) {
3240 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
3241 "cookie too big (> 4kb)");
3242 return newCookie;
3243 }
3244
3245 const char illegalNameCharacters[] = {
3246 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B,
3247 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
3248 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x00};
3249 if (aCookieAttributes.name.FindCharInSet(illegalNameCharacters, 0) != -1) {
3250 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
3251 "invalid name character");
3252 return newCookie;
3253 }
3254
3255 // domain & path checks
3256 if (!CheckDomain(aCookieAttributes, aHostURI, aKey.mBaseDomain,
3257 aRequireHostMatch)) {
3258 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
3259 "failed the domain tests");
3260 return newCookie;
3261 }
3262 if (!CheckPath(aCookieAttributes, aHostURI)) {
3263 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
3264 "failed the path tests");
3265 return newCookie;
3266 }
3267 // magic prefix checks. MUST be run after CheckDomain() and CheckPath()
3268 if (!CheckPrefixes(aCookieAttributes, isHTTPS)) {
3269 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
3270 "failed the prefix tests");
3271 return newCookie;
3272 }
3273
3274 // reject cookie if value contains an RFC 6265 disallowed character - see
3275 // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423
3276 // NOTE: this is not the full set of characters disallowed by 6265 - notably
3277 // 0x09, 0x20, 0x22, 0x2C, 0x5C, and 0x7F are missing from this list. This is
3278 // for parity with Chrome. This only applies to cookies set via the Set-Cookie
3279 // header, as document.cookie is defined to be UTF-8. Hooray for
3280 // symmetry!</sarcasm>
3281 const char illegalCharacters[] = {
3282 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C,
3283 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
3284 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x00};
3285 if (aFromHttp &&
3286 (aCookieAttributes.value.FindCharInSet(illegalCharacters, 0) != -1)) {
3287 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
3288 "invalid value character");
3289 return newCookie;
3290 }
3291
3292 // if the new cookie is httponly, make sure we're not coming from script
3293 if (!aFromHttp && aCookieAttributes.isHttpOnly) {
3294 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
3295 "cookie is httponly; coming from script");
3296 return newCookie;
3297 }
3298
3299 bool isSecure = true;
3300 if (aHostURI) {
3301 aHostURI->SchemeIs("https", &isSecure);
3302 }
3303
3304 // If the new cookie is non-https and wants to set secure flag,
3305 // browser have to ignore this new cookie.
3306 // (draft-ietf-httpbis-cookie-alone section 3.1)
3307 if (aLeaveSecureAlone && aCookieAttributes.isSecure && !isSecure) {
3308 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
3309 "non-https cookie can't set secure flag");
3310 Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
3311 BLOCKED_SECURE_SET_FROM_HTTP);
3312 return newCookie;
3313 }
3314
3315 // If the new cookie is same-site but in a cross site context,
3316 // browser must ignore the cookie.
3317 if ((aCookieAttributes.sameSite != nsICookie2::SAMESITE_UNSET) &&
3318 aThirdPartyUtil && IsSameSiteEnabled()) {
3319 // Do not treat loads triggered by web extensions as foreign
3320 bool addonAllowsLoad = false;
3321 if (aChannel) {
3322 nsCOMPtr<nsIURI> channelURI;
3323 NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI));
3324 nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo();
3325 addonAllowsLoad =
3326 loadInfo && BasePrincipal::Cast(loadInfo->TriggeringPrincipal())
3327 ->AddonAllowsLoad(channelURI);
3328 }
3329
3330 if (!addonAllowsLoad) {
3331 bool isThirdParty = false;
3332 nsresult rv = aThirdPartyUtil->IsThirdPartyChannel(aChannel, aHostURI,
3333 &isThirdParty);
3334 if (NS_FAILED(rv) || isThirdParty) {
3335 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
3336 "failed the samesite tests");
3337 return newCookie;
3338 }
3339 }
3340 }
3341
3342 aSetCookie = true;
3343 return newCookie;
3344 }
3345
3346 // processes a single cookie, and returns true if there are more cookies
3347 // to be processed
SetCookieInternal(nsIURI * aHostURI,const mozilla::nsCookieKey & aKey,bool aRequireHostMatch,CookieStatus aStatus,nsDependentCString & aCookieHeader,int64_t aServerTime,bool aFromHttp,nsIChannel * aChannel)3348 bool nsCookieService::SetCookieInternal(
3349 nsIURI *aHostURI, const mozilla::nsCookieKey &aKey, bool aRequireHostMatch,
3350 CookieStatus aStatus, nsDependentCString &aCookieHeader,
3351 int64_t aServerTime, bool aFromHttp, nsIChannel *aChannel) {
3352 NS_ASSERTION(aHostURI, "null host!");
3353 bool canSetCookie = false;
3354 nsDependentCString savedCookieHeader(aCookieHeader);
3355 nsCookieAttributes cookieAttributes;
3356 bool newCookie =
3357 CanSetCookie(aHostURI, aKey, cookieAttributes, aRequireHostMatch, aStatus,
3358 aCookieHeader, aServerTime, aFromHttp, aChannel,
3359 mLeaveSecureAlone, canSetCookie, mThirdPartyUtil);
3360
3361 if (!canSetCookie) {
3362 return newCookie;
3363 }
3364
3365 int64_t currentTimeInUsec = PR_Now();
3366 // create a new nsCookie and copy attributes
3367 RefPtr<nsCookie> cookie = nsCookie::Create(
3368 cookieAttributes.name, cookieAttributes.value, cookieAttributes.host,
3369 cookieAttributes.path, cookieAttributes.expiryTime, currentTimeInUsec,
3370 nsCookie::GenerateUniqueCreationTime(currentTimeInUsec),
3371 cookieAttributes.isSession, cookieAttributes.isSecure,
3372 cookieAttributes.isHttpOnly, aKey.mOriginAttributes,
3373 cookieAttributes.sameSite);
3374 if (!cookie) return newCookie;
3375
3376 // check permissions from site permission list, or ask the user,
3377 // to determine if we can set the cookie
3378 if (mPermissionService) {
3379 bool permission;
3380 mPermissionService->CanSetCookie(
3381 aHostURI, aChannel,
3382 static_cast<nsICookie2 *>(static_cast<nsCookie *>(cookie)),
3383 &cookieAttributes.isSession, &cookieAttributes.expiryTime, &permission);
3384 if (!permission) {
3385 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
3386 "cookie rejected by permission manager");
3387 NotifyRejected(aHostURI);
3388 return newCookie;
3389 }
3390
3391 // update isSession and expiry attributes, in case they changed
3392 cookie->SetIsSession(cookieAttributes.isSession);
3393 cookie->SetExpiry(cookieAttributes.expiryTime);
3394 }
3395
3396 // add the cookie to the list. AddInternal() takes care of logging.
3397 // we get the current time again here, since it may have changed during
3398 // prompting
3399 AddInternal(aKey, cookie, PR_Now(), aHostURI, savedCookieHeader.get(),
3400 aFromHttp);
3401 return newCookie;
3402 }
3403
3404 // this is a backend function for adding a cookie to the list, via SetCookie.
3405 // also used in the cookie manager, for profile migration from IE.
3406 // it either replaces an existing cookie; or adds the cookie to the hashtable,
3407 // and deletes a cookie (if maximum number of cookies has been
3408 // reached). also performs list maintenance by removing expired cookies.
AddInternal(const nsCookieKey & aKey,nsCookie * aCookie,int64_t aCurrentTimeInUsec,nsIURI * aHostURI,const char * aCookieHeader,bool aFromHttp)3409 void nsCookieService::AddInternal(const nsCookieKey &aKey, nsCookie *aCookie,
3410 int64_t aCurrentTimeInUsec, nsIURI *aHostURI,
3411 const char *aCookieHeader, bool aFromHttp) {
3412 MOZ_ASSERT(mInitializedDBStates);
3413 MOZ_ASSERT(mInitializedDBConn);
3414
3415 int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
3416
3417 nsListIter exactIter;
3418 bool foundCookie = false;
3419 foundCookie = FindCookie(aKey, aCookie->Host(), aCookie->Name(),
3420 aCookie->Path(), exactIter);
3421 bool foundSecureExact = foundCookie && exactIter.Cookie()->IsSecure();
3422 bool isSecure = true;
3423 if (aHostURI && NS_FAILED(aHostURI->SchemeIs("https", &isSecure))) {
3424 isSecure = false;
3425 }
3426 bool oldCookieIsSession = false;
3427 if (mLeaveSecureAlone) {
3428 // Step1, call FindSecureCookie(). FindSecureCookie() would
3429 // find the existing cookie with the security flag and has
3430 // the same name, host and path of the new cookie, if there is any.
3431 // Step2, Confirm new cookie's security setting. If any targeted
3432 // cookie had been found in Step1, then confirm whether the
3433 // new cookie could modify it. If the new created cookie’s
3434 // "secure-only-flag" is not set, and the "scheme" component
3435 // of the "request-uri" does not denote a "secure" protocol,
3436 // then ignore the new cookie.
3437 // (draft-ietf-httpbis-cookie-alone section 3.2)
3438 if (!aCookie->IsSecure() &&
3439 (foundSecureExact || FindSecureCookie(aKey, aCookie))) {
3440 if (!isSecure) {
3441 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
3442 "cookie can't save because older cookie is secure "
3443 "cookie but newer cookie is non-secure cookie");
3444 if (foundSecureExact) {
3445 Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
3446 BLOCKED_DOWNGRADE_SECURE_EXACT);
3447 } else {
3448 Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
3449 BLOCKED_DOWNGRADE_SECURE_INEXACT);
3450 }
3451 return;
3452 }
3453 // A secure site is allowed to downgrade a secure cookie
3454 // but we want to measure anyway.
3455 if (foundSecureExact) {
3456 Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
3457 DOWNGRADE_SECURE_FROM_SECURE_EXACT);
3458 } else {
3459 Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
3460 DOWNGRADE_SECURE_FROM_SECURE_INEXACT);
3461 }
3462 }
3463 }
3464
3465 RefPtr<nsCookie> oldCookie;
3466 nsCOMPtr<nsIArray> purgedList;
3467 if (foundCookie) {
3468 oldCookie = exactIter.Cookie();
3469 oldCookieIsSession = oldCookie->IsSession();
3470
3471 // Check if the old cookie is stale (i.e. has already expired). If so, we
3472 // need to be careful about the semantics of removing it and adding the new
3473 // cookie: we want the behavior wrt adding the new cookie to be the same as
3474 // if it didn't exist, but we still want to fire a removal notification.
3475 if (oldCookie->Expiry() <= currentTime) {
3476 if (aCookie->Expiry() <= currentTime) {
3477 // The new cookie has expired and the old one is stale. Nothing to do.
3478 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
3479 "cookie has already expired");
3480 return;
3481 }
3482
3483 // Remove the stale cookie. We save notification for later, once all list
3484 // modifications are complete.
3485 RemoveCookieFromList(exactIter);
3486 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
3487 "stale cookie was purged");
3488 purgedList = CreatePurgeList(oldCookie);
3489
3490 // We've done all we need to wrt removing and notifying the stale cookie.
3491 // From here on out, we pretend pretend it didn't exist, so that we
3492 // preserve expected notification semantics when adding the new cookie.
3493 foundCookie = false;
3494
3495 } else {
3496 // If the old cookie is httponly, make sure we're not coming from script.
3497 if (!aFromHttp && oldCookie->IsHttpOnly()) {
3498 COOKIE_LOGFAILURE(
3499 SET_COOKIE, aHostURI, aCookieHeader,
3500 "previously stored cookie is httponly; coming from script");
3501 return;
3502 }
3503
3504 // If the new cookie has the same value, expiry date, and isSecure,
3505 // isSession, and isHttpOnly flags then we can just keep the old one.
3506 // Only if any of these differ we would want to override the cookie.
3507 if (oldCookie->Value().Equals(aCookie->Value()) &&
3508 oldCookie->Expiry() == aCookie->Expiry() &&
3509 oldCookie->IsSecure() == aCookie->IsSecure() &&
3510 oldCookie->IsSession() == aCookie->IsSession() &&
3511 oldCookie->IsHttpOnly() == aCookie->IsHttpOnly() &&
3512 // We don't want to perform this optimization if the cookie is
3513 // considered stale, since in this case we would need to update the
3514 // database.
3515 !oldCookie->IsStale()) {
3516 // Update the last access time on the old cookie.
3517 oldCookie->SetLastAccessed(aCookie->LastAccessed());
3518 UpdateCookieOldestTime(mDBState, oldCookie);
3519 return;
3520 }
3521
3522 // Remove the old cookie.
3523 RemoveCookieFromList(exactIter);
3524
3525 // If the new cookie has expired -- i.e. the intent was simply to delete
3526 // the old cookie -- then we're done.
3527 if (aCookie->Expiry() <= currentTime) {
3528 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
3529 "previously stored cookie was deleted");
3530 NotifyChanged(oldCookie, u"deleted", oldCookieIsSession, aFromHttp);
3531 return;
3532 }
3533
3534 // Preserve creation time of cookie for ordering purposes.
3535 aCookie->SetCreationTime(oldCookie->CreationTime());
3536 }
3537
3538 } else {
3539 // check if cookie has already expired
3540 if (aCookie->Expiry() <= currentTime) {
3541 COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, aCookieHeader,
3542 "cookie has already expired");
3543 return;
3544 }
3545
3546 // check if we have to delete an old cookie.
3547 nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey);
3548 if (entry && entry->GetCookies().Length() >= mMaxCookiesPerHost) {
3549 nsListIter iter;
3550 // Prioritize evicting insecure cookies.
3551 // (draft-ietf-httpbis-cookie-alone section 3.3)
3552 mozilla::Maybe<bool> optionalSecurity =
3553 mLeaveSecureAlone ? Some(false) : Nothing();
3554 int64_t oldestCookieTime =
3555 FindStaleCookie(entry, currentTime, aHostURI, optionalSecurity, iter);
3556 if (iter.entry == nullptr) {
3557 if (aCookie->IsSecure()) {
3558 // It's valid to evict a secure cookie for another secure cookie.
3559 oldestCookieTime =
3560 FindStaleCookie(entry, currentTime, aHostURI, Some(true), iter);
3561 } else {
3562 Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
3563 EVICTING_SECURE_BLOCKED);
3564 COOKIE_LOGEVICTED(aCookie,
3565 "Too many cookies for this domain and the new "
3566 "cookie is not a secure cookie");
3567 return;
3568 }
3569 }
3570
3571 MOZ_ASSERT(iter.entry);
3572
3573 oldCookie = iter.Cookie();
3574 if (oldestCookieTime > 0 && mLeaveSecureAlone) {
3575 TelemetryForEvictingStaleCookie(oldCookie, oldestCookieTime);
3576 }
3577
3578 // remove the oldest cookie from the domain
3579 RemoveCookieFromList(iter);
3580 COOKIE_LOGEVICTED(oldCookie, "Too many cookies for this domain");
3581 purgedList = CreatePurgeList(oldCookie);
3582 } else if (mDBState->cookieCount >= ADD_TEN_PERCENT(mMaxNumberOfCookies)) {
3583 int64_t maxAge = aCurrentTimeInUsec - mDBState->cookieOldestTime;
3584 int64_t purgeAge = ADD_TEN_PERCENT(mCookiePurgeAge);
3585 if (maxAge >= purgeAge) {
3586 // we're over both size and age limits by 10%; time to purge the table!
3587 // do this by:
3588 // 1) removing expired cookies;
3589 // 2) evicting the balance of old cookies until we reach the size limit.
3590 // note that the cookieOldestTime indicator can be pessimistic - if it's
3591 // older than the actual oldest cookie, we'll just purge more eagerly.
3592 purgedList = PurgeCookies(aCurrentTimeInUsec);
3593 }
3594 }
3595 }
3596
3597 // Add the cookie to the db. We do not supply a params array for batching
3598 // because this might result in removals and additions being out of order.
3599 AddCookieToList(aKey, aCookie, mDBState, nullptr);
3600 COOKIE_LOGSUCCESS(SET_COOKIE, aHostURI, aCookieHeader, aCookie, foundCookie);
3601
3602 // Now that list mutations are complete, notify observers. We do it here
3603 // because observers may themselves attempt to mutate the list.
3604 if (purgedList) {
3605 NotifyChanged(purgedList, u"batch-deleted");
3606 }
3607
3608 NotifyChanged(aCookie, foundCookie ? u"changed" : u"added",
3609 oldCookieIsSession, aFromHttp);
3610 }
3611
3612 /******************************************************************************
3613 * nsCookieService impl:
3614 * private cookie header parsing functions
3615 ******************************************************************************/
3616
3617 // clang-format off
3618 // The following comment block elucidates the function of ParseAttributes.
3619 /******************************************************************************
3620 ** Augmented BNF, modified from RFC2109 Section 4.2.2 and RFC2616 Section 2.1
3621 ** please note: this BNF deviates from both specifications, and reflects this
3622 ** implementation. <bnf> indicates a reference to the defined grammar "bnf".
3623
3624 ** Differences from RFC2109/2616 and explanations:
3625 1. implied *LWS
3626 The grammar described by this specification is word-based. Except
3627 where noted otherwise, linear white space (<LWS>) can be included
3628 between any two adjacent words (token or quoted-string), and
3629 between adjacent words and separators, without changing the
3630 interpretation of a field.
3631 <LWS> according to spec is SP|HT|CR|LF, but here, we allow only SP | HT.
3632
3633 2. We use CR | LF as cookie separators, not ',' per spec, since ',' is in
3634 common use inside values.
3635
3636 3. tokens and values have looser restrictions on allowed characters than
3637 spec. This is also due to certain characters being in common use inside
3638 values. We allow only '=' to separate token/value pairs, and ';' to
3639 terminate tokens or values. <LWS> is allowed within tokens and values
3640 (see bug 206022).
3641
3642 4. where appropriate, full <OCTET>s are allowed, where the spec dictates to
3643 reject control chars or non-ASCII chars. This is erring on the loose
3644 side, since there's probably no good reason to enforce this strictness.
3645
3646 5. cookie <NAME> is optional, where spec requires it. This is a fairly
3647 trivial case, but allows the flexibility of setting only a cookie <VALUE>
3648 with a blank <NAME> and is required by some sites (see bug 169091).
3649
3650 6. Attribute "HttpOnly", not covered in the RFCs, is supported
3651 (see bug 178993).
3652
3653 ** Begin BNF:
3654 token = 1*<any allowed-chars except separators>
3655 value = 1*<any allowed-chars except value-sep>
3656 separators = ";" | "="
3657 value-sep = ";"
3658 cookie-sep = CR | LF
3659 allowed-chars = <any OCTET except NUL or cookie-sep>
3660 OCTET = <any 8-bit sequence of data>
3661 LWS = SP | HT
3662 NUL = <US-ASCII NUL, null control character (0)>
3663 CR = <US-ASCII CR, carriage return (13)>
3664 LF = <US-ASCII LF, linefeed (10)>
3665 SP = <US-ASCII SP, space (32)>
3666 HT = <US-ASCII HT, horizontal-tab (9)>
3667
3668 set-cookie = "Set-Cookie:" cookies
3669 cookies = cookie *( cookie-sep cookie )
3670 cookie = [NAME "="] VALUE *(";" cookie-av) ; cookie NAME/VALUE must come first
3671 NAME = token ; cookie name
3672 VALUE = value ; cookie value
3673 cookie-av = token ["=" value]
3674
3675 valid values for cookie-av (checked post-parsing) are:
3676 cookie-av = "Path" "=" value
3677 | "Domain" "=" value
3678 | "Expires" "=" value
3679 | "Max-Age" "=" value
3680 | "Comment" "=" value
3681 | "Version" "=" value
3682 | "Secure"
3683 | "HttpOnly"
3684
3685 ******************************************************************************/
3686 // clang-format on
3687
3688 // helper functions for GetTokenValue
iswhitespace(char c)3689 static inline bool iswhitespace(char c) { return c == ' ' || c == '\t'; }
isterminator(char c)3690 static inline bool isterminator(char c) { return c == '\n' || c == '\r'; }
isvalueseparator(char c)3691 static inline bool isvalueseparator(char c) {
3692 return isterminator(c) || c == ';';
3693 }
istokenseparator(char c)3694 static inline bool istokenseparator(char c) {
3695 return isvalueseparator(c) || c == '=';
3696 }
3697
3698 // Parse a single token/value pair.
3699 // Returns true if a cookie terminator is found, so caller can parse new cookie.
GetTokenValue(nsACString::const_char_iterator & aIter,nsACString::const_char_iterator & aEndIter,nsDependentCSubstring & aTokenString,nsDependentCSubstring & aTokenValue,bool & aEqualsFound)3700 bool nsCookieService::GetTokenValue(nsACString::const_char_iterator &aIter,
3701 nsACString::const_char_iterator &aEndIter,
3702 nsDependentCSubstring &aTokenString,
3703 nsDependentCSubstring &aTokenValue,
3704 bool &aEqualsFound) {
3705 nsACString::const_char_iterator start, lastSpace;
3706 // initialize value string to clear garbage
3707 aTokenValue.Rebind(aIter, aIter);
3708
3709 // find <token>, including any <LWS> between the end-of-token and the
3710 // token separator. we'll remove trailing <LWS> next
3711 while (aIter != aEndIter && iswhitespace(*aIter)) ++aIter;
3712 start = aIter;
3713 while (aIter != aEndIter && !istokenseparator(*aIter)) ++aIter;
3714
3715 // remove trailing <LWS>; first check we're not at the beginning
3716 lastSpace = aIter;
3717 if (lastSpace != start) {
3718 while (--lastSpace != start && iswhitespace(*lastSpace)) continue;
3719 ++lastSpace;
3720 }
3721 aTokenString.Rebind(start, lastSpace);
3722
3723 aEqualsFound = (*aIter == '=');
3724 if (aEqualsFound) {
3725 // find <value>
3726 while (++aIter != aEndIter && iswhitespace(*aIter)) continue;
3727
3728 start = aIter;
3729
3730 // process <token>
3731 // just look for ';' to terminate ('=' allowed)
3732 while (aIter != aEndIter && !isvalueseparator(*aIter)) ++aIter;
3733
3734 // remove trailing <LWS>; first check we're not at the beginning
3735 if (aIter != start) {
3736 lastSpace = aIter;
3737 while (--lastSpace != start && iswhitespace(*lastSpace)) continue;
3738 aTokenValue.Rebind(start, ++lastSpace);
3739 }
3740 }
3741
3742 // aIter is on ';', or terminator, or EOS
3743 if (aIter != aEndIter) {
3744 // if on terminator, increment past & return true to process new cookie
3745 if (isterminator(*aIter)) {
3746 ++aIter;
3747 return true;
3748 }
3749 // fall-through: aIter is on ';', increment and return false
3750 ++aIter;
3751 }
3752 return false;
3753 }
3754
3755 // Parses attributes from cookie header. expires/max-age attributes aren't
3756 // folded into the cookie struct here, because we don't know which one to use
3757 // until we've parsed the header.
ParseAttributes(nsDependentCString & aCookieHeader,nsCookieAttributes & aCookieAttributes)3758 bool nsCookieService::ParseAttributes(nsDependentCString &aCookieHeader,
3759 nsCookieAttributes &aCookieAttributes) {
3760 static const char kPath[] = "path";
3761 static const char kDomain[] = "domain";
3762 static const char kExpires[] = "expires";
3763 static const char kMaxage[] = "max-age";
3764 static const char kSecure[] = "secure";
3765 static const char kHttpOnly[] = "httponly";
3766 static const char kSameSite[] = "samesite";
3767 static const char kSameSiteLax[] = "lax";
3768 static const char kSameSiteStrict[] = "strict";
3769
3770 nsACString::const_char_iterator tempBegin, tempEnd;
3771 nsACString::const_char_iterator cookieStart, cookieEnd;
3772 aCookieHeader.BeginReading(cookieStart);
3773 aCookieHeader.EndReading(cookieEnd);
3774
3775 aCookieAttributes.isSecure = false;
3776 aCookieAttributes.isHttpOnly = false;
3777 aCookieAttributes.sameSite = nsICookie2::SAMESITE_UNSET;
3778
3779 nsDependentCSubstring tokenString(cookieStart, cookieStart);
3780 nsDependentCSubstring tokenValue(cookieStart, cookieStart);
3781 bool newCookie, equalsFound;
3782
3783 // extract cookie <NAME> & <VALUE> (first attribute), and copy the strings.
3784 // if we find multiple cookies, return for processing
3785 // note: if there's no '=', we assume token is <VALUE>. this is required by
3786 // some sites (see bug 169091).
3787 // XXX fix the parser to parse according to <VALUE> grammar for this case
3788 newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue,
3789 equalsFound);
3790 if (equalsFound) {
3791 aCookieAttributes.name = tokenString;
3792 aCookieAttributes.value = tokenValue;
3793 } else {
3794 aCookieAttributes.value = tokenString;
3795 }
3796
3797 // extract remaining attributes
3798 while (cookieStart != cookieEnd && !newCookie) {
3799 newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue,
3800 equalsFound);
3801
3802 if (!tokenValue.IsEmpty()) {
3803 tokenValue.BeginReading(tempBegin);
3804 tokenValue.EndReading(tempEnd);
3805 }
3806
3807 // decide which attribute we have, and copy the string
3808 if (tokenString.LowerCaseEqualsLiteral(kPath))
3809 aCookieAttributes.path = tokenValue;
3810
3811 else if (tokenString.LowerCaseEqualsLiteral(kDomain))
3812 aCookieAttributes.host = tokenValue;
3813
3814 else if (tokenString.LowerCaseEqualsLiteral(kExpires))
3815 aCookieAttributes.expires = tokenValue;
3816
3817 else if (tokenString.LowerCaseEqualsLiteral(kMaxage))
3818 aCookieAttributes.maxage = tokenValue;
3819
3820 // ignore any tokenValue for isSecure; just set the boolean
3821 else if (tokenString.LowerCaseEqualsLiteral(kSecure))
3822 aCookieAttributes.isSecure = true;
3823
3824 // ignore any tokenValue for isHttpOnly (see bug 178993);
3825 // just set the boolean
3826 else if (tokenString.LowerCaseEqualsLiteral(kHttpOnly))
3827 aCookieAttributes.isHttpOnly = true;
3828
3829 else if (tokenString.LowerCaseEqualsLiteral(kSameSite)) {
3830 if (tokenValue.LowerCaseEqualsLiteral(kSameSiteLax)) {
3831 aCookieAttributes.sameSite = nsICookie2::SAMESITE_LAX;
3832 } else if (tokenValue.LowerCaseEqualsLiteral(kSameSiteStrict)) {
3833 aCookieAttributes.sameSite = nsICookie2::SAMESITE_STRICT;
3834 }
3835 }
3836 }
3837
3838 // rebind aCookieHeader, in case we need to process another cookie
3839 aCookieHeader.Rebind(cookieStart, cookieEnd);
3840 return newCookie;
3841 }
3842
3843 /******************************************************************************
3844 * nsCookieService impl:
3845 * private domain & permission compliance enforcement functions
3846 ******************************************************************************/
3847
3848 // Get the base domain for aHostURI; e.g. for "www.bbc.co.uk", this would be
3849 // "bbc.co.uk". Only properly-formed URI's are tolerated, though a trailing
3850 // dot may be present. If aHostURI is an IP address, an alias such as
3851 // 'localhost', an eTLD such as 'co.uk', or the empty string, aBaseDomain will
3852 // be the exact host, and aRequireHostMatch will be true to indicate that
3853 // substring matches should not be performed.
GetBaseDomain(nsIEffectiveTLDService * aTLDService,nsIURI * aHostURI,nsCString & aBaseDomain,bool & aRequireHostMatch)3854 nsresult nsCookieService::GetBaseDomain(nsIEffectiveTLDService *aTLDService,
3855 nsIURI *aHostURI,
3856 nsCString &aBaseDomain,
3857 bool &aRequireHostMatch) {
3858 // get the base domain. this will fail if the host contains a leading dot,
3859 // more than one trailing dot, or is otherwise malformed.
3860 nsresult rv = aTLDService->GetBaseDomain(aHostURI, 0, aBaseDomain);
3861 aRequireHostMatch = rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
3862 rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS;
3863 if (aRequireHostMatch) {
3864 // aHostURI is either an IP address, an alias such as 'localhost', an eTLD
3865 // such as 'co.uk', or the empty string. use the host as a key in such
3866 // cases.
3867 rv = aHostURI->GetAsciiHost(aBaseDomain);
3868 }
3869 NS_ENSURE_SUCCESS(rv, rv);
3870
3871 // aHost (and thus aBaseDomain) may be the string '.'. If so, fail.
3872 if (aBaseDomain.Length() == 1 && aBaseDomain.Last() == '.')
3873 return NS_ERROR_INVALID_ARG;
3874
3875 // block any URIs without a host that aren't file:// URIs.
3876 if (aBaseDomain.IsEmpty()) {
3877 bool isFileURI = false;
3878 aHostURI->SchemeIs("file", &isFileURI);
3879 if (!isFileURI) return NS_ERROR_INVALID_ARG;
3880 }
3881
3882 return NS_OK;
3883 }
3884
3885 // Get the base domain for aHost; e.g. for "www.bbc.co.uk", this would be
3886 // "bbc.co.uk". This is done differently than GetBaseDomain(mTLDService, ): it
3887 // is assumed that aHost is already normalized, and it may contain a leading dot
3888 // (indicating that it represents a domain). A trailing dot may be present.
3889 // If aHost is an IP address, an alias such as 'localhost', an eTLD such as
3890 // 'co.uk', or the empty string, aBaseDomain will be the exact host, and a
3891 // leading dot will be treated as an error.
GetBaseDomainFromHost(nsIEffectiveTLDService * aTLDService,const nsACString & aHost,nsCString & aBaseDomain)3892 nsresult nsCookieService::GetBaseDomainFromHost(
3893 nsIEffectiveTLDService *aTLDService, const nsACString &aHost,
3894 nsCString &aBaseDomain) {
3895 // aHost must not be the string '.'.
3896 if (aHost.Length() == 1 && aHost.Last() == '.') return NS_ERROR_INVALID_ARG;
3897
3898 // aHost may contain a leading dot; if so, strip it now.
3899 bool domain = !aHost.IsEmpty() && aHost.First() == '.';
3900
3901 // get the base domain. this will fail if the host contains a leading dot,
3902 // more than one trailing dot, or is otherwise malformed.
3903 nsresult rv = aTLDService->GetBaseDomainFromHost(Substring(aHost, domain), 0,
3904 aBaseDomain);
3905 if (rv == NS_ERROR_HOST_IS_IP_ADDRESS ||
3906 rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS) {
3907 // aHost is either an IP address, an alias such as 'localhost', an eTLD
3908 // such as 'co.uk', or the empty string. use the host as a key in such
3909 // cases; however, we reject any such hosts with a leading dot, since it
3910 // doesn't make sense for them to be domain cookies.
3911 if (domain) return NS_ERROR_INVALID_ARG;
3912
3913 aBaseDomain = aHost;
3914 return NS_OK;
3915 }
3916 return rv;
3917 }
3918
3919 // Normalizes the given hostname, component by component. ASCII/ACE
3920 // components are lower-cased, and UTF-8 components are normalized per
3921 // RFC 3454 and converted to ACE.
NormalizeHost(nsCString & aHost)3922 nsresult nsCookieService::NormalizeHost(nsCString &aHost) {
3923 if (!IsASCII(aHost)) {
3924 nsAutoCString host;
3925 nsresult rv = mIDNService->ConvertUTF8toACE(aHost, host);
3926 if (NS_FAILED(rv)) return rv;
3927
3928 aHost = host;
3929 }
3930
3931 ToLowerCase(aHost);
3932 return NS_OK;
3933 }
3934
3935 // returns true if 'a' is equal to or a subdomain of 'b',
3936 // assuming no leading dots are present.
IsSubdomainOf(const nsCString & a,const nsCString & b)3937 static inline bool IsSubdomainOf(const nsCString &a, const nsCString &b) {
3938 if (a == b) return true;
3939 if (a.Length() > b.Length())
3940 return a[a.Length() - b.Length() - 1] == '.' && StringEndsWith(a, b);
3941 return false;
3942 }
3943
CheckPrefs(nsICookiePermission * aPermissionService,uint8_t aCookieBehavior,bool aThirdPartySession,bool aThirdPartyNonsecureSession,nsIURI * aHostURI,bool aIsForeign,const char * aCookieHeader,const int aNumOfCookies,const OriginAttributes & aOriginAttrs)3944 CookieStatus nsCookieService::CheckPrefs(
3945 nsICookiePermission *aPermissionService, uint8_t aCookieBehavior,
3946 bool aThirdPartySession, bool aThirdPartyNonsecureSession, nsIURI *aHostURI,
3947 bool aIsForeign, const char *aCookieHeader, const int aNumOfCookies,
3948 const OriginAttributes &aOriginAttrs) {
3949 nsresult rv;
3950
3951 // don't let ftp sites get/set cookies (could be a security issue)
3952 bool ftp;
3953 if (NS_SUCCEEDED(aHostURI->SchemeIs("ftp", &ftp)) && ftp) {
3954 COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
3955 aCookieHeader, "ftp sites cannot read cookies");
3956 return STATUS_REJECTED_WITH_ERROR;
3957 }
3958
3959 nsCOMPtr<nsIPrincipal> principal =
3960 BasePrincipal::CreateCodebasePrincipal(aHostURI, aOriginAttrs);
3961
3962 if (!principal) {
3963 COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
3964 aCookieHeader,
3965 "non-codebase principals cannot get/set cookies");
3966 return STATUS_REJECTED_WITH_ERROR;
3967 }
3968
3969 // check the permission list first; if we find an entry, it overrides
3970 // default prefs. see bug 184059.
3971 if (aPermissionService) {
3972 nsCookieAccess access;
3973 // Not passing an nsIChannel here is probably OK; our implementation
3974 // doesn't do anything with it anyway.
3975 rv = aPermissionService->CanAccess(principal, &access);
3976
3977 // if we found an entry, use it
3978 if (NS_SUCCEEDED(rv)) {
3979 switch (access) {
3980 case nsICookiePermission::ACCESS_DENY:
3981 COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
3982 aCookieHeader, "cookies are blocked for this site");
3983 return STATUS_REJECTED;
3984
3985 case nsICookiePermission::ACCESS_ALLOW:
3986 return STATUS_ACCEPTED;
3987
3988 case nsICookiePermission::ACCESS_ALLOW_FIRST_PARTY_ONLY:
3989 if (aIsForeign) {
3990 COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
3991 aCookieHeader,
3992 "third party cookies are blocked "
3993 "for this site");
3994 return STATUS_REJECTED;
3995 }
3996 return STATUS_ACCEPTED;
3997
3998 case nsICookiePermission::ACCESS_LIMIT_THIRD_PARTY:
3999 if (!aIsForeign) return STATUS_ACCEPTED;
4000 if (aNumOfCookies == 0) {
4001 COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
4002 aCookieHeader,
4003 "third party cookies are blocked "
4004 "for this site");
4005 return STATUS_REJECTED;
4006 }
4007 return STATUS_ACCEPTED;
4008 }
4009 }
4010 }
4011
4012 // check default prefs
4013 if (aCookieBehavior == nsICookieService::BEHAVIOR_REJECT) {
4014 COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
4015 aCookieHeader, "cookies are disabled");
4016 return STATUS_REJECTED;
4017 }
4018
4019 // check if cookie is foreign
4020 if (aIsForeign) {
4021 if (aCookieBehavior == nsICookieService::BEHAVIOR_REJECT_FOREIGN) {
4022 COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
4023 aCookieHeader, "context is third party");
4024 return STATUS_REJECTED;
4025 }
4026
4027 if (aCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN) {
4028 if (aNumOfCookies == 0) {
4029 COOKIE_LOGFAILURE(aCookieHeader ? SET_COOKIE : GET_COOKIE, aHostURI,
4030 aCookieHeader, "context is third party");
4031 return STATUS_REJECTED;
4032 }
4033 }
4034
4035 MOZ_ASSERT(aCookieBehavior == nsICookieService::BEHAVIOR_ACCEPT ||
4036 aCookieBehavior == nsICookieService::BEHAVIOR_LIMIT_FOREIGN);
4037
4038 if (aThirdPartySession) return STATUS_ACCEPT_SESSION;
4039
4040 if (aThirdPartyNonsecureSession) {
4041 bool isHTTPS = false;
4042 aHostURI->SchemeIs("https", &isHTTPS);
4043 if (!isHTTPS) return STATUS_ACCEPT_SESSION;
4044 }
4045 }
4046
4047 // if nothing has complained, accept cookie
4048 return STATUS_ACCEPTED;
4049 }
4050
4051 // processes domain attribute, and returns true if host has permission to set
4052 // for this domain.
CheckDomain(nsCookieAttributes & aCookieAttributes,nsIURI * aHostURI,const nsCString & aBaseDomain,bool aRequireHostMatch)4053 bool nsCookieService::CheckDomain(nsCookieAttributes &aCookieAttributes,
4054 nsIURI *aHostURI,
4055 const nsCString &aBaseDomain,
4056 bool aRequireHostMatch) {
4057 // Note: The logic in this function is mirrored in
4058 // toolkit/components/extensions/ext-cookies.js:checkSetCookiePermissions().
4059 // If it changes, please update that function, or file a bug for someone
4060 // else to do so.
4061
4062 // get host from aHostURI
4063 nsAutoCString hostFromURI;
4064 aHostURI->GetAsciiHost(hostFromURI);
4065
4066 // if a domain is given, check the host has permission
4067 if (!aCookieAttributes.host.IsEmpty()) {
4068 // Tolerate leading '.' characters, but not if it's otherwise an empty host.
4069 if (aCookieAttributes.host.Length() > 1 &&
4070 aCookieAttributes.host.First() == '.') {
4071 aCookieAttributes.host.Cut(0, 1);
4072 }
4073
4074 // switch to lowercase now, to avoid case-insensitive compares everywhere
4075 ToLowerCase(aCookieAttributes.host);
4076
4077 // check whether the host is either an IP address, an alias such as
4078 // 'localhost', an eTLD such as 'co.uk', or the empty string. in these
4079 // cases, require an exact string match for the domain, and leave the cookie
4080 // as a non-domain one. bug 105917 originally noted the requirement to deal
4081 // with IP addresses.
4082 if (aRequireHostMatch) return hostFromURI.Equals(aCookieAttributes.host);
4083
4084 // ensure the proposed domain is derived from the base domain; and also
4085 // that the host domain is derived from the proposed domain (per RFC2109).
4086 if (IsSubdomainOf(aCookieAttributes.host, aBaseDomain) &&
4087 IsSubdomainOf(hostFromURI, aCookieAttributes.host)) {
4088 // prepend a dot to indicate a domain cookie
4089 aCookieAttributes.host.InsertLiteral(".", 0);
4090 return true;
4091 }
4092
4093 /*
4094 * note: RFC2109 section 4.3.2 requires that we check the following:
4095 * that the portion of host not in domain does not contain a dot.
4096 * this prevents hosts of the form x.y.co.nz from setting cookies in the
4097 * entire .co.nz domain. however, it's only a only a partial solution and
4098 * it breaks sites (IE doesn't enforce it), so we don't perform this check.
4099 */
4100 return false;
4101 }
4102
4103 // no domain specified, use hostFromURI
4104 aCookieAttributes.host = hostFromURI;
4105 return true;
4106 }
4107
GetPathFromURI(nsIURI * aHostURI)4108 nsCString nsCookieService::GetPathFromURI(nsIURI *aHostURI) {
4109 // strip down everything after the last slash to get the path,
4110 // ignoring slashes in the query string part.
4111 // if we can QI to nsIURL, that'll take care of the query string portion.
4112 // otherwise, it's not an nsIURL and can't have a query string, so just find
4113 // the last slash.
4114 nsAutoCString path;
4115 nsCOMPtr<nsIURL> hostURL = do_QueryInterface(aHostURI);
4116 if (hostURL) {
4117 hostURL->GetDirectory(path);
4118 } else {
4119 aHostURI->GetPathQueryRef(path);
4120 int32_t slash = path.RFindChar('/');
4121 if (slash != kNotFound) {
4122 path.Truncate(slash + 1);
4123 }
4124 }
4125 return path;
4126 }
4127
CheckPath(nsCookieAttributes & aCookieAttributes,nsIURI * aHostURI)4128 bool nsCookieService::CheckPath(nsCookieAttributes &aCookieAttributes,
4129 nsIURI *aHostURI) {
4130 // if a path is given, check the host has permission
4131 if (aCookieAttributes.path.IsEmpty() ||
4132 aCookieAttributes.path.First() != '/') {
4133 aCookieAttributes.path = GetPathFromURI(aHostURI);
4134
4135 #if 0
4136 } else {
4137 /**
4138 * The following test is part of the RFC2109 spec. Loosely speaking, it says that a site
4139 * cannot set a cookie for a path that it is not on. See bug 155083. However this patch
4140 * broke several sites -- nordea (bug 155768) and citibank (bug 156725). So this test has
4141 * been disabled, unless we can evangelize these sites.
4142 */
4143 // get path from aHostURI
4144 nsAutoCString pathFromURI;
4145 if (NS_FAILED(aHostURI->GetPathQueryRef(pathFromURI)) ||
4146 !StringBeginsWith(pathFromURI, aCookieAttributes.path)) {
4147 return false;
4148 }
4149 #endif
4150 }
4151
4152 if (aCookieAttributes.path.Length() > kMaxBytesPerPath ||
4153 aCookieAttributes.path.Contains('\t'))
4154 return false;
4155
4156 return true;
4157 }
4158
4159 // CheckPrefixes
4160 //
4161 // Reject cookies whose name starts with the magic prefixes from
4162 // https://tools.ietf.org/html/draft-ietf-httpbis-cookie-prefixes-00
4163 // if they do not meet the criteria required by the prefix.
4164 //
4165 // Must not be called until after CheckDomain() and CheckPath() have
4166 // regularized and validated the nsCookieAttributes values!
CheckPrefixes(nsCookieAttributes & aCookieAttributes,bool aSecureRequest)4167 bool nsCookieService::CheckPrefixes(nsCookieAttributes &aCookieAttributes,
4168 bool aSecureRequest) {
4169 static const char kSecure[] = "__Secure-";
4170 static const char kHost[] = "__Host-";
4171 static const int kSecureLen = sizeof(kSecure) - 1;
4172 static const int kHostLen = sizeof(kHost) - 1;
4173
4174 bool isSecure =
4175 strncmp(aCookieAttributes.name.get(), kSecure, kSecureLen) == 0;
4176 bool isHost = strncmp(aCookieAttributes.name.get(), kHost, kHostLen) == 0;
4177
4178 if (!isSecure && !isHost) {
4179 // not one of the magic prefixes: carry on
4180 return true;
4181 }
4182
4183 if (!aSecureRequest || !aCookieAttributes.isSecure) {
4184 // the magic prefixes may only be used from a secure request and
4185 // the secure attribute must be set on the cookie
4186 return false;
4187 }
4188
4189 if (isHost) {
4190 // The host prefix requires that the path is "/" and that the cookie
4191 // had no domain attribute. CheckDomain() and CheckPath() MUST be run
4192 // first to make sure invalid attributes are rejected and to regularlize
4193 // them. In particular all explicit domain attributes result in a host
4194 // that starts with a dot, and if the host doesn't start with a dot it
4195 // correctly matches the true host.
4196 if (aCookieAttributes.host[0] == '.' ||
4197 !aCookieAttributes.path.EqualsLiteral("/")) {
4198 return false;
4199 }
4200 }
4201
4202 return true;
4203 }
4204
GetExpiry(nsCookieAttributes & aCookieAttributes,int64_t aServerTime,int64_t aCurrentTime)4205 bool nsCookieService::GetExpiry(nsCookieAttributes &aCookieAttributes,
4206 int64_t aServerTime, int64_t aCurrentTime) {
4207 /* Determine when the cookie should expire. This is done by taking the
4208 * difference between the server time and the time the server wants the cookie
4209 * to expire, and adding that difference to the client time. This localizes
4210 * the client time regardless of whether or not the TZ environment variable
4211 * was set on the client.
4212 *
4213 * Note: We need to consider accounting for network lag here, per RFC.
4214 */
4215 // check for max-age attribute first; this overrides expires attribute
4216 if (!aCookieAttributes.maxage.IsEmpty()) {
4217 // obtain numeric value of maxageAttribute
4218 int64_t maxage;
4219 int32_t numInts =
4220 PR_sscanf(aCookieAttributes.maxage.get(), "%lld", &maxage);
4221
4222 // default to session cookie if the conversion failed
4223 if (numInts != 1) {
4224 return true;
4225 }
4226
4227 // if this addition overflows, expiryTime will be less than currentTime
4228 // and the cookie will be expired - that's okay.
4229 aCookieAttributes.expiryTime = aCurrentTime + maxage;
4230
4231 // check for expires attribute
4232 } else if (!aCookieAttributes.expires.IsEmpty()) {
4233 PRTime expires;
4234
4235 // parse expiry time
4236 if (PR_ParseTimeString(aCookieAttributes.expires.get(), true, &expires) !=
4237 PR_SUCCESS) {
4238 return true;
4239 }
4240
4241 // If set-cookie used absolute time to set expiration, and it can't use
4242 // client time to set expiration.
4243 // Because if current time be set in the future, but the cookie expire
4244 // time be set less than current time and more than server time.
4245 // The cookie item have to be used to the expired cookie.
4246 aCookieAttributes.expiryTime = expires / int64_t(PR_USEC_PER_SEC);
4247
4248 // default to session cookie if no attributes found
4249 } else {
4250 return true;
4251 }
4252
4253 return false;
4254 }
4255
4256 /******************************************************************************
4257 * nsCookieService impl:
4258 * private cookielist management functions
4259 ******************************************************************************/
4260
RemoveAllFromMemory()4261 void nsCookieService::RemoveAllFromMemory() {
4262 // clearing the hashtable will call each nsCookieEntry's dtor,
4263 // which releases all their respective children.
4264 mDBState->hostTable.Clear();
4265 mDBState->cookieCount = 0;
4266 mDBState->cookieOldestTime = INT64_MAX;
4267 }
4268
4269 // comparator class for lastaccessed times of cookies.
4270 class CompareCookiesByAge {
4271 public:
Equals(const nsListIter & a,const nsListIter & b) const4272 bool Equals(const nsListIter &a, const nsListIter &b) const {
4273 return a.Cookie()->LastAccessed() == b.Cookie()->LastAccessed() &&
4274 a.Cookie()->CreationTime() == b.Cookie()->CreationTime();
4275 }
4276
LessThan(const nsListIter & a,const nsListIter & b) const4277 bool LessThan(const nsListIter &a, const nsListIter &b) const {
4278 // compare by lastAccessed time, and tiebreak by creationTime.
4279 int64_t result = a.Cookie()->LastAccessed() - b.Cookie()->LastAccessed();
4280 if (result != 0) return result < 0;
4281
4282 return a.Cookie()->CreationTime() < b.Cookie()->CreationTime();
4283 }
4284 };
4285
4286 // comparator class for sorting cookies by entry and index.
4287 class CompareCookiesByIndex {
4288 public:
Equals(const nsListIter & a,const nsListIter & b) const4289 bool Equals(const nsListIter &a, const nsListIter &b) const {
4290 NS_ASSERTION(a.entry != b.entry || a.index != b.index,
4291 "cookie indexes should never be equal");
4292 return false;
4293 }
4294
LessThan(const nsListIter & a,const nsListIter & b) const4295 bool LessThan(const nsListIter &a, const nsListIter &b) const {
4296 // compare by entryclass pointer, then by index.
4297 if (a.entry != b.entry) return a.entry < b.entry;
4298
4299 return a.index < b.index;
4300 }
4301 };
4302
4303 // purges expired and old cookies in a batch operation.
PurgeCookies(int64_t aCurrentTimeInUsec)4304 already_AddRefed<nsIArray> nsCookieService::PurgeCookies(
4305 int64_t aCurrentTimeInUsec) {
4306 NS_ASSERTION(mDBState->hostTable.Count() > 0, "table is empty");
4307
4308 uint32_t initialCookieCount = mDBState->cookieCount;
4309 COOKIE_LOGSTRING(
4310 LogLevel::Debug,
4311 ("PurgeCookies(): beginning purge with %" PRIu32 " cookies and %" PRId64
4312 " oldest age",
4313 mDBState->cookieCount, aCurrentTimeInUsec - mDBState->cookieOldestTime));
4314
4315 typedef nsTArray<nsListIter> PurgeList;
4316 PurgeList purgeList(kMaxNumberOfCookies);
4317
4318 nsCOMPtr<nsIMutableArray> removedList =
4319 do_CreateInstance(NS_ARRAY_CONTRACTID);
4320
4321 // Create a params array to batch the removals. This is OK here because
4322 // all the removals are in order, and there are no interleaved additions.
4323 mozIStorageAsyncStatement *stmt = mDBState->stmtDelete;
4324 nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
4325 if (mDBState->dbConn) {
4326 stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
4327 }
4328
4329 int64_t currentTime = aCurrentTimeInUsec / PR_USEC_PER_SEC;
4330 int64_t purgeTime = aCurrentTimeInUsec - mCookiePurgeAge;
4331 int64_t oldestTime = INT64_MAX;
4332
4333 for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
4334 nsCookieEntry *entry = iter.Get();
4335
4336 const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
4337 auto length = cookies.Length();
4338 for (nsCookieEntry::IndexType i = 0; i < length;) {
4339 nsListIter iter(entry, i);
4340 nsCookie *cookie = cookies[i];
4341
4342 // check if the cookie has expired
4343 if (cookie->Expiry() <= currentTime) {
4344 removedList->AppendElement(cookie);
4345 COOKIE_LOGEVICTED(cookie, "Cookie expired");
4346
4347 // remove from list; do not increment our iterator, but stop if we're
4348 // done already.
4349 gCookieService->RemoveCookieFromList(iter, paramsArray);
4350 if (i == --length) {
4351 break;
4352 }
4353 } else {
4354 // check if the cookie is over the age limit
4355 if (cookie->LastAccessed() <= purgeTime) {
4356 purgeList.AppendElement(iter);
4357
4358 } else if (cookie->LastAccessed() < oldestTime) {
4359 // reset our indicator
4360 oldestTime = cookie->LastAccessed();
4361 }
4362
4363 ++i;
4364 }
4365 MOZ_ASSERT(length == cookies.Length());
4366 }
4367 }
4368
4369 uint32_t postExpiryCookieCount = mDBState->cookieCount;
4370
4371 // now we have a list of iterators for cookies over the age limit.
4372 // sort them by age, and then we'll see how many to remove...
4373 purgeList.Sort(CompareCookiesByAge());
4374
4375 // only remove old cookies until we reach the max cookie limit, no more.
4376 uint32_t excess = mDBState->cookieCount > mMaxNumberOfCookies
4377 ? mDBState->cookieCount - mMaxNumberOfCookies
4378 : 0;
4379 if (purgeList.Length() > excess) {
4380 // We're not purging everything in the list, so update our indicator.
4381 oldestTime = purgeList[excess].Cookie()->LastAccessed();
4382
4383 purgeList.SetLength(excess);
4384 }
4385
4386 // sort the list again, this time grouping cookies with a common entryclass
4387 // together, and with ascending index. this allows us to iterate backwards
4388 // over the list removing cookies, without having to adjust indexes as we go.
4389 purgeList.Sort(CompareCookiesByIndex());
4390 for (PurgeList::index_type i = purgeList.Length(); i--;) {
4391 nsCookie *cookie = purgeList[i].Cookie();
4392 removedList->AppendElement(cookie);
4393 COOKIE_LOGEVICTED(cookie, "Cookie too old");
4394
4395 RemoveCookieFromList(purgeList[i], paramsArray);
4396 }
4397
4398 // Update the database if we have entries to purge.
4399 if (paramsArray) {
4400 uint32_t length;
4401 paramsArray->GetLength(&length);
4402 if (length) {
4403 DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
4404 NS_ASSERT_SUCCESS(rv);
4405 nsCOMPtr<mozIStoragePendingStatement> handle;
4406 rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle));
4407 NS_ASSERT_SUCCESS(rv);
4408 }
4409 }
4410
4411 // reset the oldest time indicator
4412 mDBState->cookieOldestTime = oldestTime;
4413
4414 COOKIE_LOGSTRING(
4415 LogLevel::Debug,
4416 ("PurgeCookies(): %" PRIu32 " expired; %" PRIu32 " purged; %" PRIu32
4417 " remain; %" PRId64 " oldest age",
4418 initialCookieCount - postExpiryCookieCount,
4419 postExpiryCookieCount - mDBState->cookieCount, mDBState->cookieCount,
4420 aCurrentTimeInUsec - mDBState->cookieOldestTime));
4421
4422 return removedList.forget();
4423 }
4424
4425 // find whether a given cookie has been previously set. this is provided by the
4426 // nsICookieManager interface.
4427 NS_IMETHODIMP
CookieExists(nsICookie2 * aCookie,JS::HandleValue aOriginAttributes,JSContext * aCx,uint8_t aArgc,bool * aFoundCookie)4428 nsCookieService::CookieExists(nsICookie2 *aCookie,
4429 JS::HandleValue aOriginAttributes, JSContext *aCx,
4430 uint8_t aArgc, bool *aFoundCookie) {
4431 NS_ENSURE_ARG_POINTER(aCookie);
4432 NS_ENSURE_ARG_POINTER(aCx);
4433 NS_ENSURE_ARG_POINTER(aFoundCookie);
4434 MOZ_ASSERT(aArgc == 0 || aArgc == 1);
4435
4436 OriginAttributes attrs;
4437 nsresult rv =
4438 InitializeOriginAttributes(&attrs, aOriginAttributes, aCx, aArgc,
4439 u"nsICookieManager.cookieExists()", u"2");
4440 NS_ENSURE_SUCCESS(rv, rv);
4441
4442 return CookieExistsNative(aCookie, &attrs, aFoundCookie);
4443 }
4444
NS_IMETHODIMP_(nsresult)4445 NS_IMETHODIMP_(nsresult)
4446 nsCookieService::CookieExistsNative(nsICookie2 *aCookie,
4447 OriginAttributes *aOriginAttributes,
4448 bool *aFoundCookie) {
4449 NS_ENSURE_ARG_POINTER(aCookie);
4450 NS_ENSURE_ARG_POINTER(aOriginAttributes);
4451 NS_ENSURE_ARG_POINTER(aFoundCookie);
4452
4453 if (!mDBState) {
4454 NS_WARNING("No DBState! Profile already closed?");
4455 return NS_ERROR_NOT_AVAILABLE;
4456 }
4457
4458 EnsureReadComplete(true);
4459
4460 AutoRestore<DBState *> savePrevDBState(mDBState);
4461 mDBState = (aOriginAttributes->mPrivateBrowsingId > 0) ? mPrivateDBState
4462 : mDefaultDBState;
4463
4464 nsAutoCString host, name, path;
4465 nsresult rv = aCookie->GetHost(host);
4466 NS_ENSURE_SUCCESS(rv, rv);
4467 rv = aCookie->GetName(name);
4468 NS_ENSURE_SUCCESS(rv, rv);
4469 rv = aCookie->GetPath(path);
4470 NS_ENSURE_SUCCESS(rv, rv);
4471
4472 nsAutoCString baseDomain;
4473 rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
4474 NS_ENSURE_SUCCESS(rv, rv);
4475
4476 nsListIter iter;
4477 *aFoundCookie = FindCookie(nsCookieKey(baseDomain, *aOriginAttributes), host,
4478 name, path, iter);
4479 return NS_OK;
4480 }
4481
4482 // For a given base domain, find either an expired cookie or the oldest cookie
4483 // by lastAccessed time.
FindStaleCookie(nsCookieEntry * aEntry,int64_t aCurrentTime,nsIURI * aSource,const mozilla::Maybe<bool> & aIsSecure,nsListIter & aIter)4484 int64_t nsCookieService::FindStaleCookie(nsCookieEntry *aEntry,
4485 int64_t aCurrentTime, nsIURI *aSource,
4486 const mozilla::Maybe<bool> &aIsSecure,
4487 nsListIter &aIter) {
4488 aIter.entry = nullptr;
4489 bool requireHostMatch = true;
4490 nsAutoCString baseDomain, sourceHost, sourcePath;
4491 if (aSource) {
4492 GetBaseDomain(mTLDService, aSource, baseDomain, requireHostMatch);
4493 aSource->GetAsciiHost(sourceHost);
4494 sourcePath = GetPathFromURI(aSource);
4495 }
4496
4497 const nsCookieEntry::ArrayType &cookies = aEntry->GetCookies();
4498
4499 int64_t oldestNonMatchingCookieTime = 0;
4500 nsListIter oldestNonMatchingCookie;
4501 oldestNonMatchingCookie.entry = nullptr;
4502
4503 int64_t oldestCookieTime = 0;
4504 nsListIter oldestCookie;
4505 oldestCookie.entry = nullptr;
4506
4507 int64_t actualOldestCookieTime =
4508 cookies.Length() ? cookies[0]->LastAccessed() : 0;
4509 for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
4510 nsCookie *cookie = cookies[i];
4511
4512 // If we found an expired cookie, we're done.
4513 if (cookie->Expiry() <= aCurrentTime) {
4514 aIter.entry = aEntry;
4515 aIter.index = i;
4516 return -1;
4517 }
4518
4519 int64_t lastAccessed = cookie->LastAccessed();
4520 // Record the age of the oldest cookie that is stored for this host.
4521 // oldestCookieTime is the age of the oldest cookie with a matching
4522 // secure flag, which may be more recent than an older cookie with
4523 // a non-matching secure flag.
4524 if (actualOldestCookieTime > lastAccessed) {
4525 actualOldestCookieTime = lastAccessed;
4526 }
4527 if (aIsSecure.isSome() && !aIsSecure.value()) {
4528 // We want to look for the oldest non-secure cookie first time through,
4529 // then find the oldest secure cookie the second time we are called.
4530 if (cookie->IsSecure()) {
4531 continue;
4532 }
4533 }
4534
4535 // This cookie is a candidate for eviction if we have no information about
4536 // the source request, or if it is not a path or domain match against the
4537 // source request.
4538 bool isPrimaryEvictionCandidate = true;
4539 if (aSource) {
4540 isPrimaryEvictionCandidate = !PathMatches(cookie, sourcePath) ||
4541 !DomainMatches(cookie, sourceHost);
4542 }
4543
4544 if (isPrimaryEvictionCandidate &&
4545 (!oldestNonMatchingCookie.entry ||
4546 oldestNonMatchingCookieTime > lastAccessed)) {
4547 oldestNonMatchingCookieTime = lastAccessed;
4548 oldestNonMatchingCookie.entry = aEntry;
4549 oldestNonMatchingCookie.index = i;
4550 }
4551
4552 // Check if we've found the oldest cookie so far.
4553 if (!oldestCookie.entry || oldestCookieTime > lastAccessed) {
4554 oldestCookieTime = lastAccessed;
4555 oldestCookie.entry = aEntry;
4556 oldestCookie.index = i;
4557 }
4558 }
4559
4560 // Prefer to evict the oldest cookie with a non-matching path/domain,
4561 // followed by the oldest matching cookie.
4562 if (oldestNonMatchingCookie.entry) {
4563 aIter = oldestNonMatchingCookie;
4564 } else {
4565 aIter = oldestCookie;
4566 }
4567
4568 return actualOldestCookieTime;
4569 }
4570
TelemetryForEvictingStaleCookie(nsCookie * aEvicted,int64_t oldestCookieTime)4571 void nsCookieService::TelemetryForEvictingStaleCookie(
4572 nsCookie *aEvicted, int64_t oldestCookieTime) {
4573 // We need to record the evicting cookie to telemetry.
4574 if (!aEvicted->IsSecure()) {
4575 if (aEvicted->LastAccessed() > oldestCookieTime) {
4576 Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
4577 EVICTED_NEWER_INSECURE);
4578 } else {
4579 Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
4580 EVICTED_OLDEST_COOKIE);
4581 }
4582 } else {
4583 Telemetry::Accumulate(Telemetry::COOKIE_LEAVE_SECURE_ALONE,
4584 EVICTED_PREFERRED_COOKIE);
4585 }
4586 }
4587
4588 // count the number of cookies stored by a particular host. this is provided by
4589 // the nsICookieManager interface.
4590 NS_IMETHODIMP
CountCookiesFromHost(const nsACString & aHost,uint32_t * aCountFromHost)4591 nsCookieService::CountCookiesFromHost(const nsACString &aHost,
4592 uint32_t *aCountFromHost) {
4593 if (!mDBState) {
4594 NS_WARNING("No DBState! Profile already closed?");
4595 return NS_ERROR_NOT_AVAILABLE;
4596 }
4597
4598 EnsureReadComplete(true);
4599
4600 // first, normalize the hostname, and fail if it contains illegal characters.
4601 nsAutoCString host(aHost);
4602 nsresult rv = NormalizeHost(host);
4603 NS_ENSURE_SUCCESS(rv, rv);
4604
4605 nsAutoCString baseDomain;
4606 rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
4607 NS_ENSURE_SUCCESS(rv, rv);
4608
4609 nsCookieKey key = DEFAULT_APP_KEY(baseDomain);
4610
4611 // Return a count of all cookies, including expired.
4612 nsCookieEntry *entry = mDBState->hostTable.GetEntry(key);
4613 *aCountFromHost = entry ? entry->GetCookies().Length() : 0;
4614 return NS_OK;
4615 }
4616
4617 // get an enumerator of cookies stored by a particular host. this is provided by
4618 // the nsICookieManager interface.
4619 NS_IMETHODIMP
GetCookiesFromHost(const nsACString & aHost,JS::HandleValue aOriginAttributes,JSContext * aCx,uint8_t aArgc,nsISimpleEnumerator ** aEnumerator)4620 nsCookieService::GetCookiesFromHost(const nsACString &aHost,
4621 JS::HandleValue aOriginAttributes,
4622 JSContext *aCx, uint8_t aArgc,
4623 nsISimpleEnumerator **aEnumerator) {
4624 MOZ_ASSERT(aArgc == 0 || aArgc == 1);
4625
4626 if (!mDBState) {
4627 NS_WARNING("No DBState! Profile already closed?");
4628 return NS_ERROR_NOT_AVAILABLE;
4629 }
4630
4631 EnsureReadComplete(true);
4632
4633 // first, normalize the hostname, and fail if it contains illegal characters.
4634 nsAutoCString host(aHost);
4635 nsresult rv = NormalizeHost(host);
4636 NS_ENSURE_SUCCESS(rv, rv);
4637
4638 nsAutoCString baseDomain;
4639 rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
4640 NS_ENSURE_SUCCESS(rv, rv);
4641
4642 OriginAttributes attrs;
4643 rv = InitializeOriginAttributes(&attrs, aOriginAttributes, aCx, aArgc,
4644 u"nsICookieManager.getCookiesFromHost()",
4645 u"2");
4646 NS_ENSURE_SUCCESS(rv, rv);
4647
4648 AutoRestore<DBState *> savePrevDBState(mDBState);
4649 mDBState = (attrs.mPrivateBrowsingId > 0) ? mPrivateDBState : mDefaultDBState;
4650
4651 nsCookieKey key = nsCookieKey(baseDomain, attrs);
4652
4653 nsCookieEntry *entry = mDBState->hostTable.GetEntry(key);
4654 if (!entry) return NS_NewEmptyEnumerator(aEnumerator);
4655
4656 nsCOMArray<nsICookie> cookieList(mMaxCookiesPerHost);
4657 const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
4658 for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
4659 cookieList.AppendObject(cookies[i]);
4660 }
4661
4662 return NS_NewArrayEnumerator(aEnumerator, cookieList);
4663 }
4664
4665 NS_IMETHODIMP
GetCookiesWithOriginAttributes(const nsAString & aPattern,const nsACString & aHost,nsISimpleEnumerator ** aEnumerator)4666 nsCookieService::GetCookiesWithOriginAttributes(
4667 const nsAString &aPattern, const nsACString &aHost,
4668 nsISimpleEnumerator **aEnumerator) {
4669 mozilla::OriginAttributesPattern pattern;
4670 if (!pattern.Init(aPattern)) {
4671 return NS_ERROR_INVALID_ARG;
4672 }
4673
4674 nsAutoCString host(aHost);
4675 nsresult rv = NormalizeHost(host);
4676 NS_ENSURE_SUCCESS(rv, rv);
4677
4678 nsAutoCString baseDomain;
4679 rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
4680 NS_ENSURE_SUCCESS(rv, rv);
4681
4682 return GetCookiesWithOriginAttributes(pattern, baseDomain, aEnumerator);
4683 }
4684
GetCookiesWithOriginAttributes(const mozilla::OriginAttributesPattern & aPattern,const nsCString & aBaseDomain,nsISimpleEnumerator ** aEnumerator)4685 nsresult nsCookieService::GetCookiesWithOriginAttributes(
4686 const mozilla::OriginAttributesPattern &aPattern,
4687 const nsCString &aBaseDomain, nsISimpleEnumerator **aEnumerator) {
4688 if (!mDBState) {
4689 NS_WARNING("No DBState! Profile already closed?");
4690 return NS_ERROR_NOT_AVAILABLE;
4691 }
4692 EnsureReadComplete(true);
4693
4694 AutoRestore<DBState *> savePrevDBState(mDBState);
4695 mDBState = (aPattern.mPrivateBrowsingId.WasPassed() &&
4696 aPattern.mPrivateBrowsingId.Value() > 0)
4697 ? mPrivateDBState
4698 : mDefaultDBState;
4699
4700 nsCOMArray<nsICookie> cookies;
4701 for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
4702 nsCookieEntry *entry = iter.Get();
4703
4704 if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
4705 continue;
4706 }
4707
4708 if (!aPattern.Matches(entry->mOriginAttributes)) {
4709 continue;
4710 }
4711
4712 const nsCookieEntry::ArrayType &entryCookies = entry->GetCookies();
4713
4714 for (nsCookieEntry::IndexType i = 0; i < entryCookies.Length(); ++i) {
4715 cookies.AppendObject(entryCookies[i]);
4716 }
4717 }
4718
4719 return NS_NewArrayEnumerator(aEnumerator, cookies);
4720 }
4721
4722 NS_IMETHODIMP
RemoveCookiesWithOriginAttributes(const nsAString & aPattern,const nsACString & aHost)4723 nsCookieService::RemoveCookiesWithOriginAttributes(const nsAString &aPattern,
4724 const nsACString &aHost) {
4725 MOZ_ASSERT(XRE_IsParentProcess());
4726
4727 mozilla::OriginAttributesPattern pattern;
4728 if (!pattern.Init(aPattern)) {
4729 return NS_ERROR_INVALID_ARG;
4730 }
4731
4732 nsAutoCString host(aHost);
4733 nsresult rv = NormalizeHost(host);
4734 NS_ENSURE_SUCCESS(rv, rv);
4735
4736 nsAutoCString baseDomain;
4737 rv = GetBaseDomainFromHost(mTLDService, host, baseDomain);
4738 NS_ENSURE_SUCCESS(rv, rv);
4739
4740 return RemoveCookiesWithOriginAttributes(pattern, baseDomain);
4741 }
4742
RemoveCookiesWithOriginAttributes(const mozilla::OriginAttributesPattern & aPattern,const nsCString & aBaseDomain)4743 nsresult nsCookieService::RemoveCookiesWithOriginAttributes(
4744 const mozilla::OriginAttributesPattern &aPattern,
4745 const nsCString &aBaseDomain) {
4746 if (!mDBState) {
4747 NS_WARNING("No DBState! Profile already close?");
4748 return NS_ERROR_NOT_AVAILABLE;
4749 }
4750
4751 EnsureReadComplete(true);
4752
4753 AutoRestore<DBState *> savePrevDBState(mDBState);
4754 mDBState = (aPattern.mPrivateBrowsingId.WasPassed() &&
4755 aPattern.mPrivateBrowsingId.Value() > 0)
4756 ? mPrivateDBState
4757 : mDefaultDBState;
4758
4759 mozStorageTransaction transaction(mDBState->dbConn, false);
4760 // Iterate the hash table of nsCookieEntry.
4761 for (auto iter = mDBState->hostTable.Iter(); !iter.Done(); iter.Next()) {
4762 nsCookieEntry *entry = iter.Get();
4763
4764 if (!aBaseDomain.IsEmpty() && !aBaseDomain.Equals(entry->mBaseDomain)) {
4765 continue;
4766 }
4767
4768 if (!aPattern.Matches(entry->mOriginAttributes)) {
4769 continue;
4770 }
4771
4772 // Pattern matches. Delete all cookies within this nsCookieEntry.
4773 uint32_t cookiesCount = entry->GetCookies().Length();
4774
4775 for (nsCookieEntry::IndexType i = 0; i < cookiesCount; ++i) {
4776 // Remove the first cookie from the list.
4777 nsListIter iter(entry, 0);
4778 RefPtr<nsCookie> cookie = iter.Cookie();
4779
4780 // Remove the cookie.
4781 RemoveCookieFromList(iter);
4782
4783 if (cookie) {
4784 NotifyChanged(cookie, u"deleted");
4785 }
4786 }
4787 }
4788 DebugOnly<nsresult> rv = transaction.Commit();
4789 MOZ_ASSERT(NS_SUCCEEDED(rv));
4790
4791 return NS_OK;
4792 }
4793
4794 // find an secure cookie specified by host and name
FindSecureCookie(const nsCookieKey & aKey,nsCookie * aCookie)4795 bool nsCookieService::FindSecureCookie(const nsCookieKey &aKey,
4796 nsCookie *aCookie) {
4797 nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey);
4798 if (!entry) return false;
4799
4800 const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
4801 for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
4802 nsCookie *cookie = cookies[i];
4803 // isn't a match if insecure or a different name
4804 if (!cookie->IsSecure() || !aCookie->Name().Equals(cookie->Name()))
4805 continue;
4806
4807 // The host must "domain-match" an existing cookie or vice-versa
4808 if (DomainMatches(cookie, aCookie->Host()) ||
4809 DomainMatches(aCookie, cookie->Host())) {
4810 // If the path of new cookie and the path of existing cookie
4811 // aren't "/", then this situation needs to compare paths to
4812 // ensure only that a newly-created non-secure cookie does not
4813 // overlay an existing secure cookie.
4814 if (PathMatches(cookie, aCookie->Path())) {
4815 return true;
4816 }
4817 }
4818 }
4819
4820 return false;
4821 }
4822
4823 // find an exact cookie specified by host, name, and path that hasn't expired.
FindCookie(const nsCookieKey & aKey,const nsCString & aHost,const nsCString & aName,const nsCString & aPath,nsListIter & aIter)4824 bool nsCookieService::FindCookie(const nsCookieKey &aKey,
4825 const nsCString &aHost, const nsCString &aName,
4826 const nsCString &aPath, nsListIter &aIter) {
4827 // Should |EnsureReadComplete| before.
4828 MOZ_ASSERT(mInitializedDBStates);
4829 MOZ_ASSERT(mInitializedDBConn);
4830
4831 nsCookieEntry *entry = mDBState->hostTable.GetEntry(aKey);
4832 if (!entry) return false;
4833
4834 const nsCookieEntry::ArrayType &cookies = entry->GetCookies();
4835 for (nsCookieEntry::IndexType i = 0; i < cookies.Length(); ++i) {
4836 nsCookie *cookie = cookies[i];
4837
4838 if (aHost.Equals(cookie->Host()) && aPath.Equals(cookie->Path()) &&
4839 aName.Equals(cookie->Name())) {
4840 aIter = nsListIter(entry, i);
4841 return true;
4842 }
4843 }
4844
4845 return false;
4846 }
4847
4848 // remove a cookie from the hashtable, and update the iterator state.
RemoveCookieFromList(const nsListIter & aIter,mozIStorageBindingParamsArray * aParamsArray)4849 void nsCookieService::RemoveCookieFromList(
4850 const nsListIter &aIter, mozIStorageBindingParamsArray *aParamsArray) {
4851 // if it's a non-session cookie, remove it from the db
4852 if (!aIter.Cookie()->IsSession() && mDBState->dbConn) {
4853 // Use the asynchronous binding methods to ensure that we do not acquire
4854 // the database lock.
4855 mozIStorageAsyncStatement *stmt = mDBState->stmtDelete;
4856 nsCOMPtr<mozIStorageBindingParamsArray> paramsArray(aParamsArray);
4857 if (!paramsArray) {
4858 stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
4859 }
4860
4861 nsCOMPtr<mozIStorageBindingParams> params;
4862 paramsArray->NewBindingParams(getter_AddRefs(params));
4863
4864 DebugOnly<nsresult> rv = params->BindUTF8StringByName(
4865 NS_LITERAL_CSTRING("name"), aIter.Cookie()->Name());
4866 NS_ASSERT_SUCCESS(rv);
4867
4868 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"),
4869 aIter.Cookie()->Host());
4870 NS_ASSERT_SUCCESS(rv);
4871
4872 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"),
4873 aIter.Cookie()->Path());
4874 NS_ASSERT_SUCCESS(rv);
4875
4876 nsAutoCString suffix;
4877 aIter.Cookie()->OriginAttributesRef().CreateSuffix(suffix);
4878 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"),
4879 suffix);
4880 NS_ASSERT_SUCCESS(rv);
4881
4882 rv = paramsArray->AddParams(params);
4883 NS_ASSERT_SUCCESS(rv);
4884
4885 // If we weren't given a params array, we'll need to remove it ourselves.
4886 if (!aParamsArray) {
4887 rv = stmt->BindParameters(paramsArray);
4888 NS_ASSERT_SUCCESS(rv);
4889 nsCOMPtr<mozIStoragePendingStatement> handle;
4890 rv = stmt->ExecuteAsync(mDBState->removeListener, getter_AddRefs(handle));
4891 NS_ASSERT_SUCCESS(rv);
4892 }
4893 }
4894
4895 if (aIter.entry->GetCookies().Length() == 1) {
4896 // we're removing the last element in the array - so just remove the entry
4897 // from the hash. note that the entryclass' dtor will take care of
4898 // releasing this last element for us!
4899 mDBState->hostTable.RawRemoveEntry(aIter.entry);
4900
4901 } else {
4902 // just remove the element from the list
4903 aIter.entry->GetCookies().RemoveElementAt(aIter.index);
4904 }
4905
4906 --mDBState->cookieCount;
4907 }
4908
bindCookieParameters(mozIStorageBindingParamsArray * aParamsArray,const nsCookieKey & aKey,const nsCookie * aCookie)4909 void bindCookieParameters(mozIStorageBindingParamsArray *aParamsArray,
4910 const nsCookieKey &aKey, const nsCookie *aCookie) {
4911 NS_ASSERTION(aParamsArray,
4912 "Null params array passed to bindCookieParameters!");
4913 NS_ASSERTION(aCookie, "Null cookie passed to bindCookieParameters!");
4914
4915 // Use the asynchronous binding methods to ensure that we do not acquire the
4916 // database lock.
4917 nsCOMPtr<mozIStorageBindingParams> params;
4918 DebugOnly<nsresult> rv =
4919 aParamsArray->NewBindingParams(getter_AddRefs(params));
4920 NS_ASSERT_SUCCESS(rv);
4921
4922 // Bind our values to params
4923 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("baseDomain"),
4924 aKey.mBaseDomain);
4925 NS_ASSERT_SUCCESS(rv);
4926
4927 nsAutoCString suffix;
4928 aKey.mOriginAttributes.CreateSuffix(suffix);
4929 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"),
4930 suffix);
4931 NS_ASSERT_SUCCESS(rv);
4932
4933 rv =
4934 params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), aCookie->Name());
4935 NS_ASSERT_SUCCESS(rv);
4936
4937 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("value"),
4938 aCookie->Value());
4939 NS_ASSERT_SUCCESS(rv);
4940
4941 rv =
4942 params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"), aCookie->Host());
4943 NS_ASSERT_SUCCESS(rv);
4944
4945 rv =
4946 params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"), aCookie->Path());
4947 NS_ASSERT_SUCCESS(rv);
4948
4949 rv = params->BindInt64ByName(NS_LITERAL_CSTRING("expiry"), aCookie->Expiry());
4950 NS_ASSERT_SUCCESS(rv);
4951
4952 rv = params->BindInt64ByName(NS_LITERAL_CSTRING("lastAccessed"),
4953 aCookie->LastAccessed());
4954 NS_ASSERT_SUCCESS(rv);
4955
4956 rv = params->BindInt64ByName(NS_LITERAL_CSTRING("creationTime"),
4957 aCookie->CreationTime());
4958 NS_ASSERT_SUCCESS(rv);
4959
4960 rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isSecure"),
4961 aCookie->IsSecure());
4962 NS_ASSERT_SUCCESS(rv);
4963
4964 rv = params->BindInt32ByName(NS_LITERAL_CSTRING("isHttpOnly"),
4965 aCookie->IsHttpOnly());
4966 NS_ASSERT_SUCCESS(rv);
4967
4968 rv = params->BindInt32ByName(NS_LITERAL_CSTRING("sameSite"),
4969 aCookie->SameSite());
4970 NS_ASSERT_SUCCESS(rv);
4971
4972 // Bind the params to the array.
4973 rv = aParamsArray->AddParams(params);
4974 NS_ASSERT_SUCCESS(rv);
4975 }
4976
UpdateCookieOldestTime(DBState * aDBState,nsCookie * aCookie)4977 void nsCookieService::UpdateCookieOldestTime(DBState *aDBState,
4978 nsCookie *aCookie) {
4979 if (aCookie->LastAccessed() < aDBState->cookieOldestTime) {
4980 aDBState->cookieOldestTime = aCookie->LastAccessed();
4981 }
4982 }
4983
AddCookieToList(const nsCookieKey & aKey,nsCookie * aCookie,DBState * aDBState,mozIStorageBindingParamsArray * aParamsArray,bool aWriteToDB)4984 void nsCookieService::AddCookieToList(
4985 const nsCookieKey &aKey, nsCookie *aCookie, DBState *aDBState,
4986 mozIStorageBindingParamsArray *aParamsArray, bool aWriteToDB) {
4987 NS_ASSERTION(!(aDBState->dbConn && !aWriteToDB && aParamsArray),
4988 "Not writing to the DB but have a params array?");
4989 NS_ASSERTION(!(!aDBState->dbConn && aParamsArray),
4990 "Do not have a DB connection but have a params array?");
4991
4992 if (!aCookie) {
4993 NS_WARNING("Attempting to AddCookieToList with null cookie");
4994 return;
4995 }
4996
4997 nsCookieEntry *entry = aDBState->hostTable.PutEntry(aKey);
4998 NS_ASSERTION(entry, "can't insert element into a null entry!");
4999
5000 entry->GetCookies().AppendElement(aCookie);
5001 ++aDBState->cookieCount;
5002
5003 // keep track of the oldest cookie, for when it comes time to purge
5004 UpdateCookieOldestTime(aDBState, aCookie);
5005
5006 // if it's a non-session cookie and hasn't just been read from the db, write
5007 // it out.
5008 if (aWriteToDB && !aCookie->IsSession() && aDBState->dbConn) {
5009 mozIStorageAsyncStatement *stmt = aDBState->stmtInsert;
5010 nsCOMPtr<mozIStorageBindingParamsArray> paramsArray(aParamsArray);
5011 if (!paramsArray) {
5012 stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
5013 }
5014 bindCookieParameters(paramsArray, aKey, aCookie);
5015
5016 // If we were supplied an array to store parameters, we shouldn't call
5017 // executeAsync - someone up the stack will do this for us.
5018 if (!aParamsArray) {
5019 DebugOnly<nsresult> rv = stmt->BindParameters(paramsArray);
5020 NS_ASSERT_SUCCESS(rv);
5021 nsCOMPtr<mozIStoragePendingStatement> handle;
5022 rv = stmt->ExecuteAsync(mDBState->insertListener, getter_AddRefs(handle));
5023 NS_ASSERT_SUCCESS(rv);
5024 }
5025 }
5026 }
5027
UpdateCookieInList(nsCookie * aCookie,int64_t aLastAccessed,mozIStorageBindingParamsArray * aParamsArray)5028 void nsCookieService::UpdateCookieInList(
5029 nsCookie *aCookie, int64_t aLastAccessed,
5030 mozIStorageBindingParamsArray *aParamsArray) {
5031 NS_ASSERTION(aCookie, "Passing a null cookie to UpdateCookieInList!");
5032
5033 // udpate the lastAccessed timestamp
5034 aCookie->SetLastAccessed(aLastAccessed);
5035
5036 // if it's a non-session cookie, update it in the db too
5037 if (!aCookie->IsSession() && aParamsArray) {
5038 // Create our params holder.
5039 nsCOMPtr<mozIStorageBindingParams> params;
5040 aParamsArray->NewBindingParams(getter_AddRefs(params));
5041
5042 // Bind our parameters.
5043 DebugOnly<nsresult> rv = params->BindInt64ByName(
5044 NS_LITERAL_CSTRING("lastAccessed"), aLastAccessed);
5045 NS_ASSERT_SUCCESS(rv);
5046
5047 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("name"),
5048 aCookie->Name());
5049 NS_ASSERT_SUCCESS(rv);
5050
5051 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("host"),
5052 aCookie->Host());
5053 NS_ASSERT_SUCCESS(rv);
5054
5055 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("path"),
5056 aCookie->Path());
5057 NS_ASSERT_SUCCESS(rv);
5058
5059 nsAutoCString suffix;
5060 aCookie->OriginAttributesRef().CreateSuffix(suffix);
5061 rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("originAttributes"),
5062 suffix);
5063 NS_ASSERT_SUCCESS(rv);
5064
5065 // Add our bound parameters to the array.
5066 rv = aParamsArray->AddParams(params);
5067 NS_ASSERT_SUCCESS(rv);
5068 }
5069 }
5070
SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const5071 size_t nsCookieService::SizeOfIncludingThis(
5072 mozilla::MallocSizeOf aMallocSizeOf) const {
5073 size_t n = aMallocSizeOf(this);
5074
5075 if (mDefaultDBState) {
5076 n += mDefaultDBState->SizeOfIncludingThis(aMallocSizeOf);
5077 }
5078 if (mPrivateDBState) {
5079 n += mPrivateDBState->SizeOfIncludingThis(aMallocSizeOf);
5080 }
5081
5082 return n;
5083 }
5084
MOZ_DEFINE_MALLOC_SIZE_OF(CookieServiceMallocSizeOf)5085 MOZ_DEFINE_MALLOC_SIZE_OF(CookieServiceMallocSizeOf)
5086
5087 NS_IMETHODIMP
5088 nsCookieService::CollectReports(nsIHandleReportCallback *aHandleReport,
5089 nsISupports *aData, bool aAnonymize) {
5090 MOZ_COLLECT_REPORT("explicit/cookie-service", KIND_HEAP, UNITS_BYTES,
5091 SizeOfIncludingThis(CookieServiceMallocSizeOf),
5092 "Memory used by the cookie service.");
5093
5094 return NS_OK;
5095 }
5096