Line data Source code
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 "nsIScriptError.h"
10 : #include "nsIURL.h"
11 : #include "ServiceWorkerScriptCache.h"
12 : #include "Workers.h"
13 :
14 : namespace mozilla {
15 : namespace dom {
16 : namespace workers {
17 :
18 : namespace {
19 :
20 : /**
21 : * The spec mandates slightly different behaviors for computing the scope
22 : * prefix string in case a Service-Worker-Allowed header is specified versus
23 : * when it's not available.
24 : *
25 : * With the header:
26 : * "Set maxScopeString to "/" concatenated with the strings in maxScope's
27 : * path (including empty strings), separated from each other by "/"."
28 : * Without the header:
29 : * "Set maxScopeString to "/" concatenated with the strings, except the last
30 : * string that denotes the script's file name, in registration's registering
31 : * script url's path (including empty strings), separated from each other by
32 : * "/"."
33 : *
34 : * In simpler terms, if the header is not present, we should only use the
35 : * "directory" part of the pathname, and otherwise the entire pathname should be
36 : * used. ScopeStringPrefixMode allows the caller to specify the desired
37 : * behavior.
38 : */
39 : enum ScopeStringPrefixMode {
40 : eUseDirectory,
41 : eUsePath
42 : };
43 :
44 : nsresult
45 0 : GetRequiredScopeStringPrefix(nsIURI* aScriptURI, nsACString& aPrefix,
46 : ScopeStringPrefixMode aPrefixMode)
47 : {
48 0 : nsresult rv = aScriptURI->GetPrePath(aPrefix);
49 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
50 0 : return rv;
51 : }
52 :
53 0 : if (aPrefixMode == eUseDirectory) {
54 0 : nsCOMPtr<nsIURL> scriptURL(do_QueryInterface(aScriptURI));
55 0 : if (NS_WARN_IF(!scriptURL)) {
56 0 : return NS_ERROR_FAILURE;
57 : }
58 :
59 0 : nsAutoCString dir;
60 0 : rv = scriptURL->GetDirectory(dir);
61 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
62 0 : return rv;
63 : }
64 :
65 0 : aPrefix.Append(dir);
66 0 : } else if (aPrefixMode == eUsePath) {
67 0 : nsAutoCString path;
68 0 : rv = aScriptURI->GetPath(path);
69 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
70 0 : return rv;
71 : }
72 :
73 0 : aPrefix.Append(path);
74 : } else {
75 0 : MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode");
76 : }
77 0 : return NS_OK;
78 : }
79 :
80 : } // anonymous namespace
81 :
82 : class ServiceWorkerUpdateJob::CompareCallback final : public serviceWorkerScriptCache::CompareCallback
83 : {
84 : RefPtr<ServiceWorkerUpdateJob> mJob;
85 :
86 0 : ~CompareCallback()
87 0 : {
88 0 : }
89 :
90 : public:
91 0 : explicit CompareCallback(ServiceWorkerUpdateJob* aJob)
92 0 : : mJob(aJob)
93 : {
94 0 : MOZ_ASSERT(mJob);
95 0 : }
96 :
97 : virtual void
98 0 : ComparisonResult(nsresult aStatus,
99 : bool aInCacheAndEqual,
100 : const nsAString& aNewCacheName,
101 : const nsACString& aMaxScope,
102 : nsLoadFlags aLoadFlags) override
103 : {
104 0 : mJob->ComparisonResult(aStatus,
105 : aInCacheAndEqual,
106 : aNewCacheName,
107 : aMaxScope,
108 0 : aLoadFlags);
109 0 : }
110 :
111 0 : NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateJob::CompareCallback, override)
112 : };
113 :
114 0 : class ServiceWorkerUpdateJob::ContinueUpdateRunnable final : public LifeCycleEventCallback
115 : {
116 : nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob;
117 : bool mSuccess;
118 :
119 : public:
120 0 : explicit ContinueUpdateRunnable(const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob)
121 0 : : mJob(aJob)
122 0 : , mSuccess(false)
123 : {
124 0 : AssertIsOnMainThread();
125 0 : }
126 :
127 : void
128 0 : SetResult(bool aResult) override
129 : {
130 0 : mSuccess = aResult;
131 0 : }
132 :
133 : NS_IMETHOD
134 0 : Run() override
135 : {
136 0 : AssertIsOnMainThread();
137 0 : mJob->ContinueUpdateAfterScriptEval(mSuccess);
138 0 : mJob = nullptr;
139 0 : return NS_OK;
140 : }
141 : };
142 :
143 0 : class ServiceWorkerUpdateJob::ContinueInstallRunnable final : public LifeCycleEventCallback
144 : {
145 : nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob;
146 : bool mSuccess;
147 :
148 : public:
149 0 : explicit ContinueInstallRunnable(const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob)
150 0 : : mJob(aJob)
151 0 : , mSuccess(false)
152 : {
153 0 : AssertIsOnMainThread();
154 0 : }
155 :
156 : void
157 0 : SetResult(bool aResult) override
158 : {
159 0 : mSuccess = aResult;
160 0 : }
161 :
162 : NS_IMETHOD
163 0 : Run() override
164 : {
165 0 : AssertIsOnMainThread();
166 0 : mJob->ContinueAfterInstallEvent(mSuccess);
167 0 : mJob = nullptr;
168 0 : return NS_OK;
169 : }
170 : };
171 :
172 0 : ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(nsIPrincipal* aPrincipal,
173 : const nsACString& aScope,
174 : const nsACString& aScriptSpec,
175 : nsILoadGroup* aLoadGroup,
176 0 : nsLoadFlags aLoadFlags)
177 : : ServiceWorkerJob(Type::Update, aPrincipal, aScope, aScriptSpec)
178 : , mLoadGroup(aLoadGroup)
179 0 : , mLoadFlags(aLoadFlags)
180 : {
181 0 : }
182 :
183 : already_AddRefed<ServiceWorkerRegistrationInfo>
184 0 : ServiceWorkerUpdateJob::GetRegistration() const
185 : {
186 0 : AssertIsOnMainThread();
187 0 : RefPtr<ServiceWorkerRegistrationInfo> ref = mRegistration;
188 0 : return ref.forget();
189 : }
190 :
191 0 : ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(Type aType,
192 : nsIPrincipal* aPrincipal,
193 : const nsACString& aScope,
194 : const nsACString& aScriptSpec,
195 : nsILoadGroup* aLoadGroup,
196 0 : nsLoadFlags aLoadFlags)
197 : : ServiceWorkerJob(aType, aPrincipal, aScope, aScriptSpec)
198 : , mLoadGroup(aLoadGroup)
199 0 : , mLoadFlags(aLoadFlags)
200 : {
201 0 : }
202 :
203 0 : ServiceWorkerUpdateJob::~ServiceWorkerUpdateJob()
204 : {
205 0 : }
206 :
207 : void
208 0 : ServiceWorkerUpdateJob::FailUpdateJob(ErrorResult& aRv)
209 : {
210 0 : AssertIsOnMainThread();
211 0 : MOZ_ASSERT(aRv.Failed());
212 :
213 : // Cleanup after a failed installation. This essentially implements
214 : // step 12 of the Install algorithm.
215 : //
216 : // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm
217 : //
218 : // The spec currently only runs this after an install event fails,
219 : // but we must handle many more internal errors. So we check for
220 : // cleanup on every non-successful exit.
221 0 : if (mRegistration) {
222 0 : mRegistration->ClearEvaluating();
223 0 : mRegistration->ClearInstalling();
224 :
225 0 : RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
226 0 : if (swm) {
227 0 : swm->MaybeRemoveRegistration(mRegistration);
228 : }
229 : }
230 :
231 0 : mRegistration = nullptr;
232 :
233 0 : Finish(aRv);
234 0 : }
235 :
236 : void
237 0 : ServiceWorkerUpdateJob::FailUpdateJob(nsresult aRv)
238 : {
239 0 : ErrorResult rv(aRv);
240 0 : FailUpdateJob(rv);
241 0 : }
242 :
243 : void
244 0 : ServiceWorkerUpdateJob::AsyncExecute()
245 : {
246 0 : AssertIsOnMainThread();
247 0 : MOZ_ASSERT(GetType() == Type::Update);
248 :
249 0 : RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
250 0 : if (Canceled() || !swm) {
251 0 : FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
252 0 : return;
253 : }
254 :
255 : // Begin step 1 of the Update algorithm.
256 : //
257 : // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#update-algorithm
258 :
259 : RefPtr<ServiceWorkerRegistrationInfo> registration =
260 0 : swm->GetRegistration(mPrincipal, mScope);
261 :
262 0 : if (!registration || registration->mPendingUninstall) {
263 0 : ErrorResult rv;
264 0 : rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(NS_ConvertUTF8toUTF16(mScope),
265 0 : NS_LITERAL_STRING("uninstalled"));
266 0 : FailUpdateJob(rv);
267 0 : return;
268 : }
269 :
270 : // If a Register job with a new script executed ahead of us in the job queue,
271 : // then our update for the old script no longer makes sense. Simply abort
272 : // in this case.
273 0 : RefPtr<ServiceWorkerInfo> newest = registration->Newest();
274 0 : if (newest && !mScriptSpec.Equals(newest->ScriptSpec())) {
275 0 : ErrorResult rv;
276 0 : rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(NS_ConvertUTF8toUTF16(mScope),
277 0 : NS_LITERAL_STRING("changed"));
278 0 : FailUpdateJob(rv);
279 0 : return;
280 : }
281 :
282 0 : SetRegistration(registration);
283 0 : Update();
284 : }
285 :
286 : void
287 0 : ServiceWorkerUpdateJob::SetRegistration(ServiceWorkerRegistrationInfo* aRegistration)
288 : {
289 0 : AssertIsOnMainThread();
290 :
291 0 : MOZ_ASSERT(!mRegistration);
292 0 : MOZ_ASSERT(aRegistration);
293 0 : mRegistration = aRegistration;
294 0 : }
295 :
296 : void
297 0 : ServiceWorkerUpdateJob::Update()
298 : {
299 0 : AssertIsOnMainThread();
300 0 : MOZ_ASSERT(!Canceled());
301 :
302 : // SetRegistration() must be called before Update().
303 0 : MOZ_ASSERT(mRegistration);
304 0 : MOZ_ASSERT(!mRegistration->GetInstalling());
305 :
306 : // Begin the script download and comparison steps starting at step 5
307 : // of the Update algorithm.
308 :
309 0 : RefPtr<ServiceWorkerInfo> workerInfo = mRegistration->Newest();
310 0 : nsAutoString cacheName;
311 :
312 : // If the script has not changed, we need to perform a byte-for-byte
313 : // comparison.
314 0 : if (workerInfo && workerInfo->ScriptSpec().Equals(mScriptSpec)) {
315 0 : cacheName = workerInfo->CacheName();
316 : }
317 :
318 0 : RefPtr<CompareCallback> callback = new CompareCallback(this);
319 :
320 : nsresult rv =
321 0 : serviceWorkerScriptCache::Compare(mRegistration, mPrincipal, cacheName,
322 0 : NS_ConvertUTF8toUTF16(mScriptSpec),
323 0 : callback, mLoadGroup);
324 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
325 0 : FailUpdateJob(rv);
326 0 : return;
327 : }
328 : }
329 :
330 : nsLoadFlags
331 0 : ServiceWorkerUpdateJob::GetLoadFlags() const
332 : {
333 0 : return mLoadFlags;
334 : }
335 :
336 : void
337 0 : ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus,
338 : bool aInCacheAndEqual,
339 : const nsAString& aNewCacheName,
340 : const nsACString& aMaxScope,
341 : nsLoadFlags aLoadFlags)
342 : {
343 0 : AssertIsOnMainThread();
344 :
345 0 : RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
346 0 : if (NS_WARN_IF(Canceled() || !swm)) {
347 0 : FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
348 0 : return;
349 : }
350 :
351 : // Handle failure of the download or comparison. This is part of Update
352 : // step 5 as "If the algorithm asynchronously completes with null, then:".
353 0 : if (NS_WARN_IF(NS_FAILED(aStatus))) {
354 0 : FailUpdateJob(aStatus);
355 0 : return;
356 : }
357 :
358 : // The spec validates the response before performing the byte-for-byte check.
359 : // Here we perform the comparison in another module and then validate the
360 : // script URL and scope. Make sure to do this validation before accepting
361 : // an byte-for-byte match since the service-worker-allowed header might have
362 : // changed since the last time it was installed.
363 :
364 : // This is step 2 the "validate response" section of Update algorithm step 5.
365 : // Step 1 is performed in the serviceWorkerScriptCache code.
366 :
367 0 : nsCOMPtr<nsIURI> scriptURI;
368 0 : nsresult rv = NS_NewURI(getter_AddRefs(scriptURI), mScriptSpec);
369 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
370 0 : FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
371 0 : return;
372 : }
373 :
374 0 : nsCOMPtr<nsIURI> maxScopeURI;
375 0 : if (!aMaxScope.IsEmpty()) {
376 0 : rv = NS_NewURI(getter_AddRefs(maxScopeURI), aMaxScope,
377 0 : nullptr, scriptURI);
378 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
379 0 : FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
380 0 : return;
381 : }
382 : }
383 :
384 0 : mLoadFlags = aLoadFlags;
385 :
386 0 : nsAutoCString defaultAllowedPrefix;
387 0 : rv = GetRequiredScopeStringPrefix(scriptURI, defaultAllowedPrefix,
388 0 : eUseDirectory);
389 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
390 0 : FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
391 0 : return;
392 : }
393 :
394 0 : nsAutoCString maxPrefix(defaultAllowedPrefix);
395 0 : if (maxScopeURI) {
396 0 : rv = GetRequiredScopeStringPrefix(maxScopeURI, maxPrefix, eUsePath);
397 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
398 0 : FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
399 0 : return;
400 : }
401 : }
402 :
403 0 : if (!StringBeginsWith(mRegistration->mScope, maxPrefix)) {
404 0 : nsXPIDLString message;
405 0 : NS_ConvertUTF8toUTF16 reportScope(mRegistration->mScope);
406 0 : NS_ConvertUTF8toUTF16 reportMaxPrefix(maxPrefix);
407 0 : const char16_t* params[] = { reportScope.get(), reportMaxPrefix.get() };
408 :
409 : rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
410 : "ServiceWorkerScopePathMismatch",
411 0 : params, message);
412 0 : NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to format localized string");
413 0 : swm->ReportToAllClients(mScope,
414 : message,
415 : EmptyString(),
416 : EmptyString(), 0, 0,
417 0 : nsIScriptError::errorFlag);
418 0 : FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
419 0 : return;
420 : }
421 :
422 : // The response has been validated, so now we can consider if its a
423 : // byte-for-byte match. This is step 6 of the Update algorithm.
424 0 : if (aInCacheAndEqual) {
425 0 : Finish(NS_OK);
426 0 : return;
427 : }
428 :
429 0 : Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED, 1);
430 :
431 : // Begin step 7 of the Update algorithm to evaluate the new script.
432 :
433 : RefPtr<ServiceWorkerInfo> sw =
434 0 : new ServiceWorkerInfo(mRegistration->mPrincipal,
435 0 : mRegistration->mScope,
436 : mScriptSpec,
437 : aNewCacheName,
438 0 : mLoadFlags);
439 :
440 0 : mRegistration->SetEvaluating(sw);
441 :
442 : nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle(
443 : new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(
444 0 : "ServiceWorkerUpdateJob", this));
445 0 : RefPtr<LifeCycleEventCallback> callback = new ContinueUpdateRunnable(handle);
446 :
447 0 : ServiceWorkerPrivate* workerPrivate = sw->WorkerPrivate();
448 0 : MOZ_ASSERT(workerPrivate);
449 0 : rv = workerPrivate->CheckScriptEvaluation(callback);
450 :
451 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
452 0 : FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
453 0 : return;
454 : }
455 : }
456 :
457 : void
458 0 : ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval(bool aScriptEvaluationResult)
459 : {
460 0 : AssertIsOnMainThread();
461 :
462 0 : RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
463 0 : if (Canceled() || !swm) {
464 0 : FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
465 0 : return;
466 : }
467 :
468 : // Step 7.5 of the Update algorithm verifying that the script evaluated
469 : // successfully.
470 :
471 0 : if (NS_WARN_IF(!aScriptEvaluationResult)) {
472 0 : ErrorResult error;
473 :
474 0 : NS_ConvertUTF8toUTF16 scriptSpec(mScriptSpec);
475 0 : NS_ConvertUTF8toUTF16 scope(mRegistration->mScope);
476 0 : error.ThrowTypeError<MSG_SW_SCRIPT_THREW>(scriptSpec, scope);
477 0 : FailUpdateJob(error);
478 0 : return;
479 : }
480 :
481 0 : Install(swm);
482 : }
483 :
484 : void
485 0 : ServiceWorkerUpdateJob::Install(ServiceWorkerManager* aSWM)
486 : {
487 0 : AssertIsOnMainThread();
488 0 : MOZ_DIAGNOSTIC_ASSERT(!Canceled());
489 0 : MOZ_DIAGNOSTIC_ASSERT(aSWM);
490 :
491 0 : MOZ_ASSERT(!mRegistration->GetInstalling());
492 :
493 : // Begin step 2 of the Install algorithm.
494 : //
495 : // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm
496 :
497 0 : mRegistration->TransitionEvaluatingToInstalling();
498 :
499 : // Step 6 of the Install algorithm resolving the job promise.
500 0 : InvokeResultCallbacks(NS_OK);
501 :
502 : // The job promise cannot be rejected after this point, but the job can
503 : // still fail; e.g. if the install event handler throws, etc.
504 :
505 : // fire the updatefound event
506 : nsCOMPtr<nsIRunnable> upr =
507 0 : NewRunnableMethod<RefPtr<ServiceWorkerRegistrationInfo>>(
508 : "dom::workers::ServiceWorkerManager::"
509 : "FireUpdateFoundOnServiceWorkerRegistrations",
510 : aSWM,
511 : &ServiceWorkerManager::FireUpdateFoundOnServiceWorkerRegistrations,
512 0 : mRegistration);
513 0 : NS_DispatchToMainThread(upr);
514 :
515 : // Call ContinueAfterInstallEvent(false) on main thread if the SW
516 : // script fails to load.
517 0 : nsCOMPtr<nsIRunnable> failRunnable = NewRunnableMethod<bool>(
518 : "dom::workers::ServiceWorkerUpdateJob::ContinueAfterInstallEvent",
519 : this,
520 : &ServiceWorkerUpdateJob::ContinueAfterInstallEvent,
521 0 : false);
522 :
523 : nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle(
524 : new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(
525 0 : "ServiceWorkerUpdateJob", this));
526 0 : RefPtr<LifeCycleEventCallback> callback = new ContinueInstallRunnable(handle);
527 :
528 : // Send the install event to the worker thread
529 : ServiceWorkerPrivate* workerPrivate =
530 0 : mRegistration->GetInstalling()->WorkerPrivate();
531 0 : nsresult rv = workerPrivate->SendLifeCycleEvent(NS_LITERAL_STRING("install"),
532 0 : callback, failRunnable);
533 0 : if (NS_WARN_IF(NS_FAILED(rv))) {
534 0 : ContinueAfterInstallEvent(false /* aSuccess */);
535 : }
536 0 : }
537 :
538 : void
539 0 : ServiceWorkerUpdateJob::ContinueAfterInstallEvent(bool aInstallEventSuccess)
540 : {
541 0 : if (Canceled()) {
542 0 : return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
543 : }
544 :
545 : // If we haven't been canceled we should have a registration. There appears
546 : // to be a path where it gets cleared before we call into here. Assert
547 : // to try to catch this condition, but don't crash in release.
548 0 : MOZ_DIAGNOSTIC_ASSERT(mRegistration);
549 0 : if (!mRegistration) {
550 0 : return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
551 : }
552 :
553 : // Continue executing the Install algorithm at step 12.
554 :
555 : // "If installFailed is true"
556 0 : if (NS_WARN_IF(!aInstallEventSuccess)) {
557 : // The installing worker is cleaned up by FailUpdateJob().
558 0 : FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
559 0 : return;
560 : }
561 :
562 0 : MOZ_DIAGNOSTIC_ASSERT(mRegistration->GetInstalling());
563 0 : mRegistration->TransitionInstallingToWaiting();
564 :
565 0 : Finish(NS_OK);
566 :
567 : // Step 20 calls for explicitly waiting for queued event tasks to fire. Instead,
568 : // we simply queue a runnable to execute Activate. This ensures the events are
569 : // flushed from the queue before proceeding.
570 :
571 : // Step 22 of the Install algorithm. Activate is executed after the completion
572 : // of this job. The controlling client and skipWaiting checks are performed
573 : // in TryToActivate().
574 0 : mRegistration->TryToActivateAsync();
575 : }
576 :
577 : } // namespace workers
578 : } // namespace dom
579 : } // namespace mozilla
|