-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: ensure that we capture service worker requests (#28517)
* fix: ensure that we capture service worker requests * add changelog * fix changelog * fix tests * PR comments * PR comments * PR comment * PR comment * update changelog * Update cli/CHANGELOG.md Co-authored-by: Mike McCready <[email protected]> * enable builds on all archs * fix permission issue * PR comments * Update smoke.js * Update cli/CHANGELOG.md * attempt to fix smoke tests * bump ci cache * Update smoke.js * Update smoke.js * Update example.json * fix multiple specs * fix tests * Update CHANGELOG.md --------- Co-authored-by: Mike McCready <[email protected]>
- Loading branch information
1 parent
7a9e3a4
commit c6f5e9a
Showing
43 changed files
with
1,668 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
import Debug from 'debug' | ||
import type { BrowserPreRequest } from '../../types' | ||
import type Protocol from 'devtools-protocol' | ||
|
||
const debug = Debug('cypress:proxy:service-worker-manager') | ||
|
||
type ServiceWorkerRegistration = { | ||
registrationId: string | ||
scopeURL: string | ||
activatedServiceWorker?: ServiceWorker | ||
} | ||
|
||
type ServiceWorker = { | ||
registrationId: string | ||
scriptURL: string | ||
initiatorURL?: string | ||
controlledURLs: Set<string> | ||
} | ||
|
||
type RegisterServiceWorkerOptions = { | ||
registrationId: string | ||
scopeURL: string | ||
} | ||
|
||
type UnregisterServiceWorkerOptions = { | ||
registrationId: string | ||
} | ||
|
||
type AddActivatedServiceWorkerOptions = { | ||
registrationId: string | ||
scriptURL: string | ||
} | ||
|
||
type AddInitiatorToServiceWorkerOptions = { | ||
scriptURL: string | ||
initiatorURL: string | ||
} | ||
|
||
/** | ||
* Manages service worker registrations and their controlled URLs. | ||
* | ||
* The basic lifecycle is as follows: | ||
* | ||
* 1. A service worker is registered via `registerServiceWorker`. | ||
* 2. The service worker is activated via `addActivatedServiceWorker`. | ||
* | ||
* At some point while 1 and 2 are happening: | ||
* | ||
* 3. We receive a message from the browser that a service worker has been initiated with the `addInitiatorToServiceWorker` method. | ||
* | ||
* At this point, when the manager tries to process a browser pre-request, it will check if the request is controlled by a service worker. | ||
* It determines it is controlled by a service worker if: | ||
* | ||
* 1. The document URL for the browser pre-request matches the initiator URL for the service worker. | ||
* 2. The request URL is within the scope of the service worker or the request URL's initiator is controlled by the service worker. | ||
*/ | ||
export class ServiceWorkerManager { | ||
private serviceWorkerRegistrations: Map<string, ServiceWorkerRegistration> = new Map<string, ServiceWorkerRegistration>() | ||
private pendingInitiators: Map<string, string> = new Map<string, string>() | ||
|
||
/** | ||
* Goes through the list of service worker registrations and adds or removes them from the manager. | ||
*/ | ||
updateServiceWorkerRegistrations (data: Protocol.ServiceWorker.WorkerRegistrationUpdatedEvent) { | ||
data.registrations.forEach((registration) => { | ||
if (registration.isDeleted) { | ||
this.unregisterServiceWorker({ registrationId: registration.registrationId }) | ||
} else { | ||
this.registerServiceWorker({ registrationId: registration.registrationId, scopeURL: registration.scopeURL }) | ||
} | ||
}) | ||
} | ||
|
||
/** | ||
* Goes through the list of service worker versions and adds any that are activated to the manager. | ||
*/ | ||
updateServiceWorkerVersions (data: Protocol.ServiceWorker.WorkerVersionUpdatedEvent) { | ||
data.versions.forEach((version) => { | ||
if (version.status === 'activated') { | ||
this.addActivatedServiceWorker({ registrationId: version.registrationId, scriptURL: version.scriptURL }) | ||
} | ||
}) | ||
} | ||
|
||
/** | ||
* Adds an initiator URL to a service worker. If the service worker has not yet been activated, the initiator URL is added to a pending list and will | ||
* be added to the service worker when it is activated. | ||
*/ | ||
addInitiatorToServiceWorker ({ scriptURL, initiatorURL }: AddInitiatorToServiceWorkerOptions) { | ||
let initiatorAdded = false | ||
|
||
for (const registration of this.serviceWorkerRegistrations.values()) { | ||
if (registration.activatedServiceWorker?.scriptURL === scriptURL) { | ||
registration.activatedServiceWorker.initiatorURL = initiatorURL | ||
|
||
initiatorAdded = true | ||
break | ||
} | ||
} | ||
|
||
if (!initiatorAdded) { | ||
this.pendingInitiators.set(scriptURL, initiatorURL) | ||
} | ||
} | ||
|
||
/** | ||
* Processes a browser pre-request to determine if it is controlled by a service worker. If it is, the service worker's controlled URLs are updated with the given request URL. | ||
* | ||
* @param browserPreRequest The browser pre-request to process. | ||
* @returns `true` if the request is controlled by a service worker, `false` otherwise. | ||
*/ | ||
processBrowserPreRequest (browserPreRequest: BrowserPreRequest) { | ||
if (browserPreRequest.initiator?.type === 'preload') { | ||
return false | ||
} | ||
|
||
let requestControlledByServiceWorker = false | ||
|
||
this.serviceWorkerRegistrations.forEach((registration) => { | ||
const activatedServiceWorker = registration.activatedServiceWorker | ||
const paramlessDocumentURL = browserPreRequest.documentURL.split('?')[0] | ||
|
||
if (!activatedServiceWorker || activatedServiceWorker.initiatorURL !== paramlessDocumentURL) { | ||
return | ||
} | ||
|
||
const paramlessURL = browserPreRequest.url.split('?')[0] | ||
const paramlessInitiatorURL = browserPreRequest.initiator?.url?.split('?')[0] | ||
const paramlessCallStackURL = browserPreRequest.initiator?.stack?.callFrames[0]?.url?.split('?')[0] | ||
const urlIsControlled = paramlessURL.startsWith(registration.scopeURL) | ||
const initiatorUrlIsControlled = paramlessInitiatorURL && activatedServiceWorker.controlledURLs?.has(paramlessInitiatorURL) | ||
const topStackUrlIsControlled = paramlessCallStackURL && activatedServiceWorker.controlledURLs?.has(paramlessCallStackURL) | ||
|
||
if (urlIsControlled || initiatorUrlIsControlled || topStackUrlIsControlled) { | ||
activatedServiceWorker.controlledURLs.add(paramlessURL) | ||
requestControlledByServiceWorker = true | ||
} | ||
}) | ||
|
||
return requestControlledByServiceWorker | ||
} | ||
|
||
/** | ||
* Registers the given service worker with the given scope. Will not overwrite an existing registration. | ||
*/ | ||
private registerServiceWorker ({ registrationId, scopeURL }: RegisterServiceWorkerOptions) { | ||
// Only register service workers if they haven't already been registered | ||
if (this.serviceWorkerRegistrations.get(registrationId)?.scopeURL === scopeURL) { | ||
return | ||
} | ||
|
||
this.serviceWorkerRegistrations.set(registrationId, { | ||
registrationId, | ||
scopeURL, | ||
}) | ||
} | ||
|
||
/** | ||
* Unregisters the service worker with the given registration ID. | ||
*/ | ||
private unregisterServiceWorker ({ registrationId }: UnregisterServiceWorkerOptions) { | ||
this.serviceWorkerRegistrations.delete(registrationId) | ||
} | ||
|
||
/** | ||
* Adds an activated service worker to the manager. | ||
*/ | ||
private addActivatedServiceWorker ({ registrationId, scriptURL }: AddActivatedServiceWorkerOptions) { | ||
const registration = this.serviceWorkerRegistrations.get(registrationId) | ||
|
||
if (registration) { | ||
const initiatorURL = this.pendingInitiators.get(scriptURL) | ||
|
||
registration.activatedServiceWorker = { | ||
registrationId, | ||
scriptURL, | ||
controlledURLs: new Set<string>(), | ||
initiatorURL: initiatorURL || registration.activatedServiceWorker?.initiatorURL, | ||
} | ||
|
||
this.pendingInitiators.delete(scriptURL) | ||
} else { | ||
debug('Could not find service worker registration for registration ID %s', registrationId) | ||
} | ||
} | ||
} |
Oops, something went wrong.
c6f5e9a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circle has built the
linux x64
version of the Test Runner.Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version
Run this command to install the pre-release locally:
c6f5e9a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circle has built the
linux arm64
version of the Test Runner.Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version
Run this command to install the pre-release locally:
c6f5e9a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circle has built the
darwin x64
version of the Test Runner.Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version
Run this command to install the pre-release locally:
c6f5e9a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circle has built the
darwin arm64
version of the Test Runner.Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version
Run this command to install the pre-release locally:
c6f5e9a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circle has built the
win32 x64
version of the Test Runner.Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version
Run this command to install the pre-release locally: