ServiceWorkerUpdateJob.cpp (19019B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "ServiceWorkerUpdateJob.h" 8 9 #include "ServiceWorkerManager.h" 10 #include "ServiceWorkerPrivate.h" 11 #include "ServiceWorkerRegistrationInfo.h" 12 #include "ServiceWorkerScriptCache.h" 13 #include "mozilla/ProfilerMarkers.h" 14 #include "mozilla/dom/WorkerCommon.h" 15 #include "nsIScriptError.h" 16 #include "nsIURL.h" 17 #include "nsNetUtil.h" 18 #include "nsProxyRelease.h" 19 20 namespace mozilla::dom { 21 22 using serviceWorkerScriptCache::OnFailure; 23 24 namespace { 25 26 /** 27 * The spec mandates slightly different behaviors for computing the scope 28 * prefix string in case a Service-Worker-Allowed header is specified versus 29 * when it's not available. 30 * 31 * With the header: 32 * "Set maxScopeString to "/" concatenated with the strings in maxScope's 33 * path (including empty strings), separated from each other by "/"." 34 * Without the header: 35 * "Set maxScopeString to "/" concatenated with the strings, except the last 36 * string that denotes the script's file name, in registration's registering 37 * script url's path (including empty strings), separated from each other by 38 * "/"." 39 * 40 * In simpler terms, if the header is not present, we should only use the 41 * "directory" part of the pathname, and otherwise the entire pathname should be 42 * used. ScopeStringPrefixMode allows the caller to specify the desired 43 * behavior. 44 */ 45 enum ScopeStringPrefixMode { eUseDirectory, eUsePath }; 46 47 nsresult GetRequiredScopeStringPrefix(nsIURI* aScriptURI, nsACString& aPrefix, 48 ScopeStringPrefixMode aPrefixMode) { 49 nsresult rv; 50 if (aPrefixMode == eUseDirectory) { 51 nsCOMPtr<nsIURL> scriptURL(do_QueryInterface(aScriptURI)); 52 if (NS_WARN_IF(!scriptURL)) { 53 return NS_ERROR_FAILURE; 54 } 55 56 rv = scriptURL->GetDirectory(aPrefix); 57 if (NS_WARN_IF(NS_FAILED(rv))) { 58 return rv; 59 } 60 } else if (aPrefixMode == eUsePath) { 61 rv = aScriptURI->GetPathQueryRef(aPrefix); 62 if (NS_WARN_IF(NS_FAILED(rv))) { 63 return rv; 64 } 65 } else { 66 MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode"); 67 } 68 return NS_OK; 69 } 70 71 } // anonymous namespace 72 73 class ServiceWorkerUpdateJob::CompareCallback final 74 : public serviceWorkerScriptCache::CompareCallback { 75 RefPtr<ServiceWorkerUpdateJob> mJob; 76 77 ~CompareCallback() = default; 78 79 public: 80 explicit CompareCallback(ServiceWorkerUpdateJob* aJob) : mJob(aJob) { 81 MOZ_ASSERT(mJob); 82 } 83 84 virtual void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual, 85 OnFailure aOnFailure, 86 const nsAString& aNewCacheName, 87 const nsACString& aMaxScope, 88 nsLoadFlags aLoadFlags) override { 89 mJob->ComparisonResult(aStatus, aInCacheAndEqual, aOnFailure, aNewCacheName, 90 aMaxScope, aLoadFlags); 91 } 92 93 NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateJob::CompareCallback, override) 94 }; 95 96 class ServiceWorkerUpdateJob::ContinueUpdateRunnable final 97 : public LifeCycleEventCallback { 98 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob; 99 bool mSuccess; 100 101 public: 102 explicit ContinueUpdateRunnable( 103 const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob) 104 : mJob(aJob), mSuccess(false) { 105 MOZ_ASSERT(NS_IsMainThread()); 106 } 107 108 void SetResult(bool aResult) override { mSuccess = aResult; } 109 110 NS_IMETHOD 111 Run() override { 112 MOZ_ASSERT(NS_IsMainThread()); 113 mJob->ContinueUpdateAfterScriptEval(mSuccess); 114 mJob = nullptr; 115 return NS_OK; 116 } 117 }; 118 119 class ServiceWorkerUpdateJob::ContinueInstallRunnable final 120 : public LifeCycleEventCallback { 121 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob; 122 bool mSuccess; 123 124 public: 125 explicit ContinueInstallRunnable( 126 const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob) 127 : mJob(aJob), mSuccess(false) { 128 MOZ_ASSERT(NS_IsMainThread()); 129 } 130 131 void SetResult(bool aResult) override { mSuccess = aResult; } 132 133 NS_IMETHOD 134 Run() override { 135 MOZ_ASSERT(NS_IsMainThread()); 136 mJob->ContinueAfterInstallEvent(mSuccess); 137 mJob = nullptr; 138 return NS_OK; 139 } 140 }; 141 142 ServiceWorkerUpdateJob::ServiceWorkerUpdateJob( 143 nsIPrincipal* aPrincipal, const nsACString& aScope, nsCString aScriptSpec, 144 ServiceWorkerUpdateViaCache aUpdateViaCache, 145 const ServiceWorkerLifetimeExtension& aLifetimeExtension) 146 : ServiceWorkerUpdateJob(Type::Update, aPrincipal, aScope, 147 std::move(aScriptSpec), aUpdateViaCache, 148 aLifetimeExtension) {} 149 150 already_AddRefed<ServiceWorkerRegistrationInfo> 151 ServiceWorkerUpdateJob::GetRegistration() const { 152 MOZ_ASSERT(NS_IsMainThread()); 153 RefPtr<ServiceWorkerRegistrationInfo> ref = mRegistration; 154 return ref.forget(); 155 } 156 157 ServiceWorkerUpdateJob::ServiceWorkerUpdateJob( 158 Type aType, nsIPrincipal* aPrincipal, const nsACString& aScope, 159 nsCString aScriptSpec, ServiceWorkerUpdateViaCache aUpdateViaCache, 160 const ServiceWorkerLifetimeExtension& aLifetimeExtension) 161 : ServiceWorkerJob(aType, aPrincipal, aScope, std::move(aScriptSpec)), 162 mUpdateViaCache(aUpdateViaCache), 163 mLifetimeExtension(aLifetimeExtension), 164 mOnFailure(serviceWorkerScriptCache::OnFailure::DoNothing) {} 165 166 ServiceWorkerUpdateJob::~ServiceWorkerUpdateJob() = default; 167 168 void ServiceWorkerUpdateJob::FailUpdateJob(ErrorResult& aRv) { 169 MOZ_ASSERT(NS_IsMainThread()); 170 MOZ_ASSERT(aRv.Failed()); 171 172 // Cleanup after a failed installation. This essentially implements 173 // step 13 of the Install algorithm. 174 // 175 // https://w3c.github.io/ServiceWorker/#installation-algorithm 176 // 177 // The spec currently only runs this after an install event fails, 178 // but we must handle many more internal errors. So we check for 179 // cleanup on every non-successful exit. 180 if (mRegistration) { 181 // Some kinds of failures indicate there is something broken in the 182 // currently installed registration. In these cases we want to fully 183 // unregister. 184 if (mOnFailure == OnFailure::Uninstall) { 185 mRegistration->ClearAsCorrupt(); 186 } 187 188 // Otherwise just clear the workers we may have created as part of the 189 // update process. 190 else { 191 mRegistration->ClearEvaluating(); 192 mRegistration->ClearInstalling(); 193 } 194 195 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); 196 if (swm) { 197 swm->MaybeRemoveRegistration(mRegistration); 198 199 // Also clear the registration on disk if we are forcing uninstall 200 // due to a particularly bad failure. 201 if (mOnFailure == OnFailure::Uninstall) { 202 swm->MaybeSendUnregister(mRegistration->Principal(), 203 mRegistration->Scope()); 204 } 205 } 206 } 207 208 mRegistration = nullptr; 209 210 Finish(aRv); 211 } 212 213 void ServiceWorkerUpdateJob::FailUpdateJob(nsresult aRv) { 214 ErrorResult rv(aRv); 215 FailUpdateJob(rv); 216 // This signature is intentionally about not using the result, so we do need 217 // to suppress the exception. 218 rv.SuppressException(); 219 } 220 221 void ServiceWorkerUpdateJob::AsyncExecute() { 222 AUTO_PROFILER_MARKER_UNTYPED("ServiceWorkerUpdateJob::AsyncExecute", DOM, {}); 223 224 MOZ_ASSERT(NS_IsMainThread()); 225 MOZ_ASSERT(GetType() == Type::Update); 226 227 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); 228 if (Canceled() || !swm) { 229 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); 230 return; 231 } 232 233 // Invoke Update algorithm: 234 // https://w3c.github.io/ServiceWorker/#update-algorithm 235 // 236 // "Let registration be the result of running the Get Registration algorithm 237 // passing job’s scope url as the argument." 238 RefPtr<ServiceWorkerRegistrationInfo> registration = 239 swm->GetRegistration(mPrincipal, mScope); 240 241 if (!registration) { 242 ErrorResult rv; 243 rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "uninstalled"); 244 FailUpdateJob(rv); 245 return; 246 } 247 248 // "Let newestWorker be the result of running Get Newest Worker algorithm 249 // passing registration as the argument." 250 RefPtr<ServiceWorkerInfo> newest = registration->Newest(); 251 252 // "If job’s job type is update, and newestWorker is not null and its script 253 // url does not equal job’s script url, then: 254 // 1. Invoke Reject Job Promise with job and TypeError. 255 // 2. Invoke Finish Job with job and abort these steps." 256 if (newest && !newest->ScriptSpec().Equals(mScriptSpec)) { 257 ErrorResult rv; 258 rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "changed"); 259 FailUpdateJob(rv); 260 return; 261 } 262 263 SetRegistration(registration); 264 Update(); 265 } 266 267 void ServiceWorkerUpdateJob::SetRegistration( 268 ServiceWorkerRegistrationInfo* aRegistration) { 269 MOZ_ASSERT(NS_IsMainThread()); 270 271 MOZ_ASSERT(!mRegistration); 272 MOZ_ASSERT(aRegistration); 273 mRegistration = aRegistration; 274 } 275 276 void ServiceWorkerUpdateJob::Update() { 277 AUTO_PROFILER_MARKER_UNTYPED("ServiceWorkerUpdateJob::Update", DOM, {}); 278 279 MOZ_ASSERT(NS_IsMainThread()); 280 MOZ_ASSERT(!Canceled()); 281 282 // SetRegistration() must be called before Update(). 283 MOZ_ASSERT(mRegistration); 284 MOZ_ASSERT(!mRegistration->GetInstalling()); 285 286 // Begin the script download and comparison steps starting at step 5 287 // of the Update algorithm. 288 289 RefPtr<ServiceWorkerInfo> workerInfo = mRegistration->Newest(); 290 nsAutoString cacheName; 291 292 // If the script has not changed, we need to perform a byte-for-byte 293 // comparison. 294 if (workerInfo && workerInfo->ScriptSpec().Equals(mScriptSpec)) { 295 cacheName = workerInfo->CacheName(); 296 } 297 298 RefPtr<CompareCallback> callback = new CompareCallback(this); 299 300 nsresult rv = serviceWorkerScriptCache::Compare( 301 mRegistration, mPrincipal, cacheName, mScriptSpec, callback); 302 if (NS_WARN_IF(NS_FAILED(rv))) { 303 FailUpdateJob(rv); 304 return; 305 } 306 } 307 308 ServiceWorkerUpdateViaCache ServiceWorkerUpdateJob::GetUpdateViaCache() const { 309 return mUpdateViaCache; 310 } 311 312 void ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus, 313 bool aInCacheAndEqual, 314 OnFailure aOnFailure, 315 const nsAString& aNewCacheName, 316 const nsACString& aMaxScope, 317 nsLoadFlags aLoadFlags) { 318 MOZ_ASSERT(NS_IsMainThread()); 319 320 mOnFailure = aOnFailure; 321 322 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); 323 if (NS_WARN_IF(Canceled() || !swm)) { 324 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); 325 return; 326 } 327 328 // Handle failure of the download or comparison. This is part of Update 329 // step 5 as "If the algorithm asynchronously completes with null, then:". 330 if (NS_WARN_IF(NS_FAILED(aStatus))) { 331 FailUpdateJob(aStatus); 332 return; 333 } 334 335 // The spec validates the response before performing the byte-for-byte check. 336 // Here we perform the comparison in another module and then validate the 337 // script URL and scope. Make sure to do this validation before accepting 338 // an byte-for-byte match since the service-worker-allowed header might have 339 // changed since the last time it was installed. 340 341 // This is step 2 the "validate response" section of Update algorithm step 5. 342 // Step 1 is performed in the serviceWorkerScriptCache code. 343 344 nsCOMPtr<nsIURI> scriptURI; 345 nsresult rv = NS_NewURI(getter_AddRefs(scriptURI), mScriptSpec); 346 if (NS_WARN_IF(NS_FAILED(rv))) { 347 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); 348 return; 349 } 350 351 nsCOMPtr<nsIURI> maxScopeURI; 352 if (!aMaxScope.IsEmpty()) { 353 rv = NS_NewURI(getter_AddRefs(maxScopeURI), aMaxScope, nullptr, scriptURI); 354 if (NS_WARN_IF(NS_FAILED(rv))) { 355 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); 356 return; 357 } 358 } 359 360 nsAutoCString defaultAllowedPrefix; 361 rv = GetRequiredScopeStringPrefix(scriptURI, defaultAllowedPrefix, 362 eUseDirectory); 363 if (NS_WARN_IF(NS_FAILED(rv))) { 364 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); 365 return; 366 } 367 368 nsAutoCString maxPrefix(defaultAllowedPrefix); 369 if (maxScopeURI) { 370 rv = GetRequiredScopeStringPrefix(maxScopeURI, maxPrefix, eUsePath); 371 if (NS_WARN_IF(NS_FAILED(rv))) { 372 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); 373 return; 374 } 375 } 376 377 nsCOMPtr<nsIURI> scopeURI; 378 rv = NS_NewURI(getter_AddRefs(scopeURI), mRegistration->Scope(), nullptr, 379 scriptURI); 380 if (NS_WARN_IF(NS_FAILED(rv))) { 381 FailUpdateJob(NS_ERROR_FAILURE); 382 return; 383 } 384 385 nsAutoCString scopeString; 386 rv = scopeURI->GetPathQueryRef(scopeString); 387 if (NS_WARN_IF(NS_FAILED(rv))) { 388 FailUpdateJob(NS_ERROR_FAILURE); 389 return; 390 } 391 392 if (!StringBeginsWith(scopeString, maxPrefix)) { 393 nsAutoString message; 394 NS_ConvertUTF8toUTF16 reportScope(mRegistration->Scope()); 395 NS_ConvertUTF8toUTF16 reportMaxPrefix(maxPrefix); 396 397 rv = nsContentUtils::FormatLocalizedString( 398 message, nsContentUtils::eDOM_PROPERTIES, 399 "ServiceWorkerScopePathMismatch", reportScope, reportMaxPrefix); 400 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to format localized string"); 401 swm->ReportToAllClients(mScope, message, ""_ns, u""_ns, 0, 0, 402 nsIScriptError::errorFlag); 403 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); 404 return; 405 } 406 407 // The response has been validated, so now we can consider if its a 408 // byte-for-byte match. This is step 6 of the Update algorithm. 409 if (aInCacheAndEqual) { 410 Finish(NS_OK); 411 return; 412 } 413 414 // Begin step 7 of the Update algorithm to evaluate the new script. 415 nsLoadFlags flags = aLoadFlags; 416 if (GetUpdateViaCache() == ServiceWorkerUpdateViaCache::None) { 417 flags |= nsIRequest::VALIDATE_ALWAYS; 418 } 419 420 RefPtr<ServiceWorkerInfo> sw = new ServiceWorkerInfo( 421 mRegistration->Principal(), mRegistration->Scope(), mRegistration->Type(), 422 mRegistration->Id(), mRegistration->Version(), mScriptSpec, aNewCacheName, 423 flags); 424 425 // If the registration is corrupt enough to force an uninstall if the 426 // upgrade fails, then we want to make sure the upgrade takes effect 427 // if it succeeds. Therefore force the skip-waiting flag on to replace 428 // the broken worker after install. 429 if (aOnFailure == OnFailure::Uninstall) { 430 sw->SetSkipWaitingFlag(); 431 } 432 433 mRegistration->SetEvaluating(sw); 434 435 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle( 436 new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>( 437 "ServiceWorkerUpdateJob", this)); 438 RefPtr<LifeCycleEventCallback> callback = new ContinueUpdateRunnable(handle); 439 440 ServiceWorkerPrivate* workerPrivate = sw->WorkerPrivate(); 441 MOZ_ASSERT(workerPrivate); 442 // Note that there are some synchronous failure cases that may immediately 443 // invoke the callback, meaning that FailUpdateJob may have already been 444 // called before this method returns. 445 rv = workerPrivate->CheckScriptEvaluation(mLifetimeExtension, callback); 446 447 // We call FailUpdateJob because it is idempotent and as defense-in-depth 448 // against early errors returns potentially being introduced above that return 449 // with ensuring that the passed-in callback will be invoked (such as those 450 // that are frequently added for shutdown phases). 451 if (NS_WARN_IF(NS_FAILED(rv))) { 452 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); 453 return; 454 } 455 } 456 457 void ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval( 458 bool aScriptEvaluationResult) { 459 MOZ_ASSERT(NS_IsMainThread()); 460 461 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); 462 if (Canceled() || !swm) { 463 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); 464 return; 465 } 466 467 // Step 7.5 of the Update algorithm verifying that the script evaluated 468 // successfully. 469 470 if (NS_WARN_IF(!aScriptEvaluationResult)) { 471 ErrorResult error; 472 error.ThrowTypeError<MSG_SW_SCRIPT_THREW>(mScriptSpec, 473 mRegistration->Scope()); 474 FailUpdateJob(error); 475 return; 476 } 477 478 Install(); 479 } 480 481 void ServiceWorkerUpdateJob::Install() { 482 AUTO_PROFILER_MARKER_UNTYPED("ServiceWorkerUpdateJob::Install", DOM, {}); 483 484 MOZ_ASSERT(NS_IsMainThread()); 485 MOZ_DIAGNOSTIC_ASSERT(!Canceled()); 486 487 MOZ_ASSERT(!mRegistration->GetInstalling()); 488 489 // Begin step 2 of the Install algorithm. 490 // 491 // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm 492 493 mRegistration->TransitionEvaluatingToInstalling(); 494 495 // Step 6 of the Install algorithm resolving the job promise. 496 InvokeResultCallbacks(NS_OK); 497 498 // Queue a task to fire an event named updatefound at all the 499 // ServiceWorkerRegistration. 500 mRegistration->FireUpdateFound(); 501 502 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle( 503 new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>( 504 "ServiceWorkerUpdateJob", this)); 505 RefPtr<LifeCycleEventCallback> callback = new ContinueInstallRunnable(handle); 506 507 // Send the install event to the worker thread 508 ServiceWorkerPrivate* workerPrivate = 509 mRegistration->GetInstalling()->WorkerPrivate(); 510 nsresult rv = workerPrivate->SendLifeCycleEvent(u"install"_ns, 511 mLifetimeExtension, callback); 512 if (NS_WARN_IF(NS_FAILED(rv))) { 513 ContinueAfterInstallEvent(false /* aSuccess */); 514 } 515 } 516 517 void ServiceWorkerUpdateJob::ContinueAfterInstallEvent( 518 bool aInstallEventSuccess) { 519 if (Canceled()) { 520 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); 521 } 522 523 // If we haven't been canceled we should have a registration. There appears 524 // to be a path where it gets cleared before we call into here. Assert 525 // to try to catch this condition, but don't crash in release. 526 MOZ_DIAGNOSTIC_ASSERT(mRegistration); 527 if (!mRegistration) { 528 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); 529 } 530 531 // Continue executing the Install algorithm at step 12. 532 533 // "If installFailed is true" 534 if (NS_WARN_IF(!aInstallEventSuccess)) { 535 // The installing worker is cleaned up by FailUpdateJob(). 536 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); 537 return; 538 } 539 540 // Abort the update Job if the installWorker is null (e.g. when an extension 541 // is shutting down and all its workers have been terminated). 542 if (!mRegistration->GetInstalling()) { 543 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); 544 } 545 546 mRegistration->TransitionInstallingToWaiting(); 547 548 Finish(NS_OK); 549 550 // Step 20 calls for explicitly waiting for queued event tasks to fire. 551 // Instead, we simply queue a runnable to execute Activate. This ensures the 552 // events are flushed from the queue before proceeding. 553 554 // Step 22 of the Install algorithm. Activate is executed after the 555 // completion of this job. The controlling client and skipWaiting checks are 556 // performed in TryToActivate(). 557 mRegistration->TryToActivateAsync(mLifetimeExtension); 558 } 559 560 } // namespace mozilla::dom