sourcemonkey 0.0.0-stage → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- package/skills/SourceMonkey-friction/references/template.md +59 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
// SW-side `GM_xmlhttpRequest` handler. Drives `fetch` against the
|
|
2
|
+
// page's request, sends events back to the originating tab via
|
|
3
|
+
// `chrome.tabs.sendMessage`. Supports text and binary request bodies
|
|
4
|
+
// and response types (text/json/blob/arraybuffer/document/stream),
|
|
5
|
+
// and `@connect` URL gating.
|
|
6
|
+
// Requests are credentialed (the user's cookies go out) unless the
|
|
7
|
+
// caller passes `anonymous: true` — see `runXhr`.
|
|
8
|
+
// Headers `fetch` refuses to send (Origin / Cookie / Referer /
|
|
9
|
+
// Sec-Fetch-*) travel as a declarativeNetRequest rule instead — see
|
|
10
|
+
// `gm-xhr-headers.ts`.
|
|
11
|
+
//
|
|
12
|
+
// Wire protocol:
|
|
13
|
+
//
|
|
14
|
+
// (The payload types are in gm-xhr-types.ts.)
|
|
15
|
+
//
|
|
16
|
+
// Page → SW: `sm:gm-xhr-start`
|
|
17
|
+
// { type, id, scriptId, request: GmXhrRequestPayload }
|
|
18
|
+
//
|
|
19
|
+
// Page → SW: `sm:gm-xhr-abort`
|
|
20
|
+
// { type, id }
|
|
21
|
+
//
|
|
22
|
+
// SW → page: `sm:gm-xhr-event`
|
|
23
|
+
// { type, id, event: GmXhrStreamEvent | GmXhrEvent }
|
|
24
|
+
// The stream events (headers, then the body's pieces) first, then
|
|
25
|
+
// one terminal event.
|
|
26
|
+
//
|
|
27
|
+
// `id` is a per-request UUID-ish string the page generates. The SW
|
|
28
|
+
// just round-trips it.
|
|
29
|
+
//
|
|
30
|
+
// The response body streams to the page in `progress` pieces ahead of
|
|
31
|
+
// the terminal `load` (see `streamBody`), so the worker never holds the
|
|
32
|
+
// whole of it and no size cap is needed.
|
|
33
|
+
//
|
|
34
|
+
// One safety limit lives here:
|
|
35
|
+
// - `@connect` redirect re-validation: `fetch` follows redirects by
|
|
36
|
+
// default, so an allowlisted host could 302 to a disallowed one.
|
|
37
|
+
// The caller passes `checkFinalUrl`; if the response came back
|
|
38
|
+
// from a redirected URL the policy refuses (the user can be asked
|
|
39
|
+
// about it, as for the first URL), we drop the body and return the
|
|
40
|
+
// policy's error instead of handing it to the script.
|
|
41
|
+
// This blocks *reading* responses from non-allowlisted hosts; it
|
|
42
|
+
// can't prevent the request reaching the redirect target (fetch
|
|
43
|
+
// gives no per-hop hook and `redirect:'manual'` yields an opaque
|
|
44
|
+
// response with no readable Location), so the outbound hop still
|
|
45
|
+
// happens — but no disallowed-host data flows back to the script.
|
|
46
|
+
// That hop carries the user's cookies, so a script can provoke a
|
|
47
|
+
// credentialed request to an off-allowlist host it can't read the
|
|
48
|
+
// answer from. Blind, but a real side effect.
|
|
49
|
+
//
|
|
50
|
+
// AbortController per request. Stored in a Map keyed by id so an
|
|
51
|
+
// aborted request can be cancelled mid-fetch.
|
|
52
|
+
import { base64ToBytes, bytesToBase64 } from '../bytes.js';
|
|
53
|
+
import { headerRuleFailedMessage, headerStallMessage, redirectRefusedMessage, unreachableMessage, } from '../gm-connect.js';
|
|
54
|
+
import { buildHeaderRule, splitHeaders, whenStaleRulesCleared, } from './gm-xhr-headers.js';
|
|
55
|
+
const activeXhrs = new Map();
|
|
56
|
+
// How long a request may go without *response headers* before the
|
|
57
|
+
// worker gives up on it. Deliberately not a request timeout:
|
|
58
|
+
//
|
|
59
|
+
// - `GM_xmlhttpRequest` has no default `timeout`, matching TM/VM, and
|
|
60
|
+
// adding one would kill slow but healthy transfers — a 50 MB body
|
|
61
|
+
// on a bad connection can legitimately run for minutes.
|
|
62
|
+
// - What has to be bounded is the *header* phase, because that is
|
|
63
|
+
// exactly what the request's header rule waits on: the rule is
|
|
64
|
+
// removed the moment `fetch` resolves, so a slow body holds
|
|
65
|
+
// nothing, while a server that accepts the connection and never
|
|
66
|
+
// answers would keep a rule (and its id) alive indefinitely.
|
|
67
|
+
// - So the timer covers time-to-first-byte only, and is cleared as
|
|
68
|
+
// soon as headers arrive. A download that takes an hour after that
|
|
69
|
+
// is left alone.
|
|
70
|
+
//
|
|
71
|
+
// Two minutes is far past any healthy time-to-first-byte and well under
|
|
72
|
+
// the page-side `XHR_PAGE_SIDE_TTL_MS` (5 minutes, in
|
|
73
|
+
// `wrapper-gm-runtime.js`), so the worker reports a wedged request
|
|
74
|
+
// itself rather than racing the page's own giving-up. Keep it that way:
|
|
75
|
+
// if it ever exceeds the page TTL, both would fire and a script would
|
|
76
|
+
// see whichever won.
|
|
77
|
+
const XHR_HEADER_STALL_MS = 2 * 60 * 1000;
|
|
78
|
+
// How much of the body goes in one `progress` message, at most. Big
|
|
79
|
+
// enough that a large download isn't thousands of messages, small
|
|
80
|
+
// enough that the worker never holds much of it and a message stays far
|
|
81
|
+
// under Chrome's 64 MiB limit (base64 adds a third).
|
|
82
|
+
const MAX_PIECE_BYTES = 1024 * 1024;
|
|
83
|
+
// One run of the body's pieces into a single array.
|
|
84
|
+
function joinPieces(pieces, total) {
|
|
85
|
+
if (pieces.length === 1)
|
|
86
|
+
return pieces[0];
|
|
87
|
+
const out = new Uint8Array(total);
|
|
88
|
+
let offset = 0;
|
|
89
|
+
for (const p of pieces) {
|
|
90
|
+
out.set(p, offset);
|
|
91
|
+
offset += p.byteLength;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
// The body's length when the response says it, for `lengthComputable`.
|
|
96
|
+
// Not with a Content-Encoding: Content-Length then counts the encoded
|
|
97
|
+
// bytes, and the reader hands over decoded ones.
|
|
98
|
+
function bodyLength(headers) {
|
|
99
|
+
const encoding = headers.get('content-encoding');
|
|
100
|
+
if (encoding && encoding.trim().toLowerCase() !== 'identity')
|
|
101
|
+
return null;
|
|
102
|
+
const length = Number(headers.get('content-length'));
|
|
103
|
+
return headers.has('content-length') && Number.isFinite(length) && length >= 0 ? length : null;
|
|
104
|
+
}
|
|
105
|
+
// Render the response headers as a single newline-joined string —
|
|
106
|
+
// TM/VM convention; userscripts call `getResponseHeader(name)`-style
|
|
107
|
+
// helpers on this. Each header is `Name: value\r\n`; we use `\r\n`
|
|
108
|
+
// per spec but `\n` would also work.
|
|
109
|
+
function serializeHeaders(headers) {
|
|
110
|
+
const lines = [];
|
|
111
|
+
headers.forEach((value, name) => {
|
|
112
|
+
lines.push(`${name}: ${value}`);
|
|
113
|
+
});
|
|
114
|
+
// Trailing CRLF after the last header — TM/VM behavior. Userscripts
|
|
115
|
+
// that split on \r\n and iterate get the expected N entries (rather
|
|
116
|
+
// than N-1 trailing-token-eaten).
|
|
117
|
+
return lines.length === 0 ? '' : lines.join('\r\n') + '\r\n';
|
|
118
|
+
}
|
|
119
|
+
// A stream event that couldn't be delivered: the page that asked is
|
|
120
|
+
// gone (closed, or navigated away). Chrome's own error says so in its
|
|
121
|
+
// terms; this says it in ours.
|
|
122
|
+
class PageGoneError extends Error {
|
|
123
|
+
constructor() {
|
|
124
|
+
super('The page that made the request is gone, so the request was stopped');
|
|
125
|
+
this.name = 'PageGoneError';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// A request that got no response at all, said in our words.
|
|
129
|
+
class UnreachableError extends Error {
|
|
130
|
+
constructor(url) {
|
|
131
|
+
super(unreachableMessage(URL.parse(url)?.host || url));
|
|
132
|
+
this.name = 'UnreachableError';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Run a fetch on behalf of the page. Always resolves to the terminal
|
|
136
|
+
// event to send back, including for aborts and timeouts — the page-side
|
|
137
|
+
// runtime fires its own `onabort` immediately as well, so a script sees
|
|
138
|
+
// that one first.
|
|
139
|
+
//
|
|
140
|
+
// Before that, `push` delivers the stream events: the headers, then the
|
|
141
|
+
// body in pieces as it arrives (`streamBody`). Each push is awaited, so
|
|
142
|
+
// they arrive in order and a slow page holds back the reading rather
|
|
143
|
+
// than letting the worker buffer the body. A push that throws (the tab
|
|
144
|
+
// is gone) aborts the request.
|
|
145
|
+
//
|
|
146
|
+
// `checkFinalUrl`, when supplied, re-checks the response's URL against
|
|
147
|
+
// the script's `@connect` policy after redirects, resolving to null when
|
|
148
|
+
// allowed or else the error to report. A response that landed on a
|
|
149
|
+
// refused host (via a redirect the initial check couldn't see) is
|
|
150
|
+
// dropped with that error rather than returned.
|
|
151
|
+
//
|
|
152
|
+
// Request headers the browser's `fetch` refuses to send (`Origin`,
|
|
153
|
+
// `Cookie`, `Sec-Fetch-*`) go out by a short-lived declarativeNetRequest
|
|
154
|
+
// rule around the request (docs/xhr-header-surgery.md).
|
|
155
|
+
export async function runXhr(id, request, push, checkFinalUrl, tabId = null) {
|
|
156
|
+
// Taken over from `reserveXhr` when the caller made one, so an abort
|
|
157
|
+
// that came while the request waited on the policy counts.
|
|
158
|
+
const controller = activeXhrs.get(id)?.controller ?? new AbortController();
|
|
159
|
+
activeXhrs.set(id, { controller, tabId });
|
|
160
|
+
if (controller.signal.aborted) {
|
|
161
|
+
activeXhrs.delete(id);
|
|
162
|
+
return { kind: 'abort' };
|
|
163
|
+
}
|
|
164
|
+
// Timeout: setTimeout that aborts the controller. We track the
|
|
165
|
+
// timer so we can clear it on completion.
|
|
166
|
+
let timeoutFired = false;
|
|
167
|
+
let timeoutTimer = null;
|
|
168
|
+
let headerStallFired = false;
|
|
169
|
+
let headerStallTimer = null;
|
|
170
|
+
// Every path out of an aborted controller decides the terminal event
|
|
171
|
+
// here. The script's `timeout`, the header-stall backstop and a plain
|
|
172
|
+
// `abort()` all abort the *same* controller, so the shadow flags are
|
|
173
|
+
// the only thing separating them.
|
|
174
|
+
//
|
|
175
|
+
// The script's own timeout outranks the backstop: if both fired, the
|
|
176
|
+
// one it asked for is the one it should hear about.
|
|
177
|
+
const terminalAbortEvent = () => {
|
|
178
|
+
if (timeoutFired)
|
|
179
|
+
return { kind: 'timeout' };
|
|
180
|
+
if (headerStallFired) {
|
|
181
|
+
return { kind: 'error', message: headerStallMessage(XHR_HEADER_STALL_MS) };
|
|
182
|
+
}
|
|
183
|
+
return { kind: 'abort' };
|
|
184
|
+
};
|
|
185
|
+
if (request.timeout && request.timeout > 0) {
|
|
186
|
+
timeoutTimer = setTimeout(() => {
|
|
187
|
+
timeoutFired = true;
|
|
188
|
+
controller.abort();
|
|
189
|
+
}, request.timeout);
|
|
190
|
+
}
|
|
191
|
+
const clearHeaderStall = () => {
|
|
192
|
+
if (headerStallTimer != null) {
|
|
193
|
+
clearTimeout(headerStallTimer);
|
|
194
|
+
headerStallTimer = null;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
// Header-phase backstop. Armed just before the rule install rather
|
|
198
|
+
// than just before the fetch: the install is the only other await
|
|
199
|
+
// that could wedge, and covering it costs nothing since it normally
|
|
200
|
+
// takes a millisecond.
|
|
201
|
+
const startHeaderStall = () => {
|
|
202
|
+
headerStallTimer = setTimeout(() => {
|
|
203
|
+
headerStallFired = true;
|
|
204
|
+
controller.abort();
|
|
205
|
+
}, XHR_HEADER_STALL_MS);
|
|
206
|
+
};
|
|
207
|
+
// Headers `fetch` would drop travel as a DNR rule instead; the rest
|
|
208
|
+
// ride along on the request. `installedRuleId` is held so the rule
|
|
209
|
+
// comes off however the request ends.
|
|
210
|
+
const { passthrough, surgical } = splitHeaders(request.headers);
|
|
211
|
+
let installedRuleId = null;
|
|
212
|
+
try {
|
|
213
|
+
const method = (request.method ?? 'GET').toUpperCase();
|
|
214
|
+
// Built once: `buildHeaderRule` allocates an id, so asking it twice
|
|
215
|
+
// would burn one and answer a different question the second time.
|
|
216
|
+
// Where there is a rule, `spec.target` is the URL to fetch — the
|
|
217
|
+
// same tagged string the rule matches on, which is what makes the
|
|
218
|
+
// rule apply to this request and no other.
|
|
219
|
+
const spec = buildHeaderRule(request.url, surgical, method);
|
|
220
|
+
startHeaderStall();
|
|
221
|
+
if (spec) {
|
|
222
|
+
installedRuleId = await installHeaderRule(spec);
|
|
223
|
+
// Refuse rather than send something the script didn't ask for.
|
|
224
|
+
// The headers it wanted are the ones the browser won't let it
|
|
225
|
+
// set, so a silent fallback isn't "the request minus a header": a
|
|
226
|
+
// `Cookie` override that doesn't apply sends the user's real
|
|
227
|
+
// session instead of the script's — the opposite of the ask.
|
|
228
|
+
// A rule that exists only to strip our own `Origin` is not the
|
|
229
|
+
// script's request, so that one falls back quietly.
|
|
230
|
+
if (installedRuleId == null && Object.keys(surgical).length > 0) {
|
|
231
|
+
return { kind: 'error', message: headerRuleFailedMessage() };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const init = {
|
|
235
|
+
method,
|
|
236
|
+
headers: Object.keys(passthrough).length > 0 ? passthrough : undefined,
|
|
237
|
+
// Credentialed by default, matching Tampermonkey/Violentmonkey —
|
|
238
|
+
// the point of GM_xmlhttpRequest is usually to read another site
|
|
239
|
+
// *as the signed-in user*, and `anonymous: true` opts out.
|
|
240
|
+
//
|
|
241
|
+
// 'include' is stated rather than left to the default because the
|
|
242
|
+
// default's behavior here isn't ours to rely on: Chrome already
|
|
243
|
+
// attaches cookies to an extension fetch aimed at a host the
|
|
244
|
+
// extension has permission for (we hold `<all_urls>`), so
|
|
245
|
+
// 'same-origin' happens to send them today. That is a property of
|
|
246
|
+
// the host-permission grant, not of the request we wrote. Saying
|
|
247
|
+
// 'include' pins the intent and gives `anonymous` a symmetric
|
|
248
|
+
// counterpart.
|
|
249
|
+
//
|
|
250
|
+
// The `@connect` allowlist is what gates which hosts a script may
|
|
251
|
+
// act as the user on.
|
|
252
|
+
credentials: request.anonymous ? 'omit' : 'include',
|
|
253
|
+
signal: controller.signal,
|
|
254
|
+
};
|
|
255
|
+
// GET / HEAD with no body — fetch() rejects "body not allowed".
|
|
256
|
+
if (init.method !== 'GET' && init.method !== 'HEAD') {
|
|
257
|
+
if (request.body)
|
|
258
|
+
init.body = requestBody(request.body);
|
|
259
|
+
else if (request.data != null)
|
|
260
|
+
init.body = typeof request.data === 'string' ? request.data : String(request.data);
|
|
261
|
+
}
|
|
262
|
+
if (request.nocache)
|
|
263
|
+
init.cache = 'no-store';
|
|
264
|
+
else if (request.revalidate)
|
|
265
|
+
init.cache = 'no-cache';
|
|
266
|
+
// 'manual' keeps the redirect from being followed, and the response
|
|
267
|
+
// it gives says it was one.
|
|
268
|
+
if (request.redirect === 'error')
|
|
269
|
+
init.redirect = 'manual';
|
|
270
|
+
// The tagged URL where there's a rule: the tag is what the rule
|
|
271
|
+
// matches on, and it never leaves the browser (a fragment isn't
|
|
272
|
+
// transmitted, and `res.url` comes back without it).
|
|
273
|
+
//
|
|
274
|
+
// Chrome's message for a network failure is exactly "Failed to
|
|
275
|
+
// fetch"; other `TypeError`s (a header name `fetch` won't take)
|
|
276
|
+
// name their own problem and pass through.
|
|
277
|
+
const res = await fetch(spec?.target ?? request.url, init).catch((err) => {
|
|
278
|
+
if (!controller.signal.aborted && err instanceof TypeError && err.message === 'Failed to fetch') {
|
|
279
|
+
throw new UnreachableError(request.url);
|
|
280
|
+
}
|
|
281
|
+
throw err;
|
|
282
|
+
});
|
|
283
|
+
if (res.type === 'opaqueredirect') {
|
|
284
|
+
return { kind: 'error', message: redirectRefusedMessage() };
|
|
285
|
+
}
|
|
286
|
+
// Headers are in, so the stall backstop has nothing left to guard —
|
|
287
|
+
// whatever the body does from here is the download's business.
|
|
288
|
+
clearHeaderStall();
|
|
289
|
+
// The rule has done its job the moment the request is on the wire,
|
|
290
|
+
// and `fetch` resolving means the response headers are already
|
|
291
|
+
// back. Dropping it here rather than in `finally` keeps a slow
|
|
292
|
+
// download from holding a rule (and its id) for the whole transfer.
|
|
293
|
+
if (installedRuleId != null) {
|
|
294
|
+
const ruleId = installedRuleId;
|
|
295
|
+
installedRuleId = null;
|
|
296
|
+
await removeHeaderRule(ruleId);
|
|
297
|
+
}
|
|
298
|
+
// @connect redirect re-validation: if fetch followed a redirect to
|
|
299
|
+
// a host the policy doesn't allow, refuse to read or return the
|
|
300
|
+
// body. (See the file header for why the outbound hop can't be
|
|
301
|
+
// prevented here.) We only check on actual redirects — an
|
|
302
|
+
// un-redirected response is on the host the initial check already
|
|
303
|
+
// approved.
|
|
304
|
+
const refusal = res.redirected && checkFinalUrl ? await checkFinalUrl(res.url, controller.signal) : null;
|
|
305
|
+
// Aborted (or timed out) while the check asked the user.
|
|
306
|
+
if (controller.signal.aborted)
|
|
307
|
+
return terminalAbortEvent();
|
|
308
|
+
if (refusal !== null)
|
|
309
|
+
return { kind: 'error', message: refusal };
|
|
310
|
+
const total = bodyLength(res.headers);
|
|
311
|
+
const responseHeaders = serializeHeaders(res.headers);
|
|
312
|
+
// A push that fails means the page is gone; said in our words.
|
|
313
|
+
const pushOrStop = (event) => push(event).catch(() => {
|
|
314
|
+
throw new PageGoneError();
|
|
315
|
+
});
|
|
316
|
+
await pushOrStop({
|
|
317
|
+
kind: 'headers', status: res.status, statusText: res.statusText, finalUrl: res.url, responseHeaders,
|
|
318
|
+
mimeType: res.headers.get('content-type') ?? '', total,
|
|
319
|
+
});
|
|
320
|
+
await streamBody(res, total, pushOrStop, controller);
|
|
321
|
+
// The body went ahead in `progress` events; the page-side runtime
|
|
322
|
+
// builds `responseText` and `response` from them.
|
|
323
|
+
return {
|
|
324
|
+
kind: 'load',
|
|
325
|
+
response: {
|
|
326
|
+
status: res.status,
|
|
327
|
+
statusText: res.statusText,
|
|
328
|
+
responseURL: res.url,
|
|
329
|
+
responseText: '',
|
|
330
|
+
response: null,
|
|
331
|
+
responseMimeType: res.headers.get('content-type') ?? '',
|
|
332
|
+
responseHeaders,
|
|
333
|
+
finalUrl: res.url,
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
// Distinguish abort vs. timeout vs. error. AbortController-driven
|
|
339
|
+
// aborts surface as DOMException name='AbortError'; we use a
|
|
340
|
+
// shadow flag set by the timeout handler to know which one.
|
|
341
|
+
const isAbortError = err instanceof Error && (err.name === 'AbortError' || /aborted/i.test(err.message));
|
|
342
|
+
// The script's own timeout outranks everything, then the aborts.
|
|
343
|
+
if (timeoutFired)
|
|
344
|
+
return terminalAbortEvent();
|
|
345
|
+
if (err instanceof PageGoneError || err instanceof UnreachableError) {
|
|
346
|
+
return { kind: 'error', message: err.message };
|
|
347
|
+
}
|
|
348
|
+
if (headerStallFired || isAbortError)
|
|
349
|
+
return terminalAbortEvent();
|
|
350
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
351
|
+
return { kind: 'error', message };
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
if (timeoutTimer != null)
|
|
355
|
+
clearTimeout(timeoutTimer);
|
|
356
|
+
clearHeaderStall();
|
|
357
|
+
activeXhrs.delete(id);
|
|
358
|
+
// Ends a body nobody read (a redirect refused, a page gone before
|
|
359
|
+
// the headers went out), which would otherwise hold its connection.
|
|
360
|
+
// Nothing once the request is done.
|
|
361
|
+
controller.abort();
|
|
362
|
+
// Backstop for the abort, timeout and error paths — the success
|
|
363
|
+
// path already dropped it. `removeHeaderRule` swallows its own
|
|
364
|
+
// failures, so this can't reject out of the `finally`.
|
|
365
|
+
if (installedRuleId != null)
|
|
366
|
+
await removeHeaderRule(installedRuleId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// A non-text request body as `fetch` takes it. `fetch` sets the
|
|
370
|
+
// Content-Type from it (a Blob's type, the multipart boundary, or
|
|
371
|
+
// form-urlencoded) unless the script's headers set one.
|
|
372
|
+
function requestBody(body) {
|
|
373
|
+
switch (body.kind) {
|
|
374
|
+
case 'bytes':
|
|
375
|
+
return new Blob([base64ToBytes(body.data)], { type: body.type });
|
|
376
|
+
case 'params':
|
|
377
|
+
return new URLSearchParams(body.data);
|
|
378
|
+
case 'form': {
|
|
379
|
+
const form = new FormData();
|
|
380
|
+
for (const entry of body.entries) {
|
|
381
|
+
if ('value' in entry)
|
|
382
|
+
form.append(entry.name, entry.value);
|
|
383
|
+
else
|
|
384
|
+
form.append(entry.name, new Blob([base64ToBytes(entry.data)], { type: entry.type }), entry.filename);
|
|
385
|
+
}
|
|
386
|
+
return form;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Read the body and push it in pieces. A piece goes out as soon as the
|
|
391
|
+
// previous push is done, holding whatever arrived meanwhile (up to
|
|
392
|
+
// MAX_PIECE_BYTES), so a trickle of small reads still reaches the page
|
|
393
|
+
// promptly while a fast download goes out in large pieces. Reading
|
|
394
|
+
// waits only when a full piece is already waiting behind a push, so the
|
|
395
|
+
// worker holds at most about two pieces.
|
|
396
|
+
//
|
|
397
|
+
// A push resolves once the page's listeners have run (none of them
|
|
398
|
+
// answers with `sendResponse`), which is what makes it a brake: an
|
|
399
|
+
// isolated-world page takes each piece as it comes. A page-world
|
|
400
|
+
// script's pieces go through the bridge, which only re-posts them, so
|
|
401
|
+
// there the brake is the bridge, not the script.
|
|
402
|
+
//
|
|
403
|
+
// A failed push aborts `controller` at once, so a read still waiting on
|
|
404
|
+
// the network ends now rather than with the next chunk.
|
|
405
|
+
async function streamBody(res, total, push, controller) {
|
|
406
|
+
const reader = res.body?.getReader();
|
|
407
|
+
// No body at all (a 204, a HEAD).
|
|
408
|
+
if (!reader)
|
|
409
|
+
return;
|
|
410
|
+
// In an object, as the push's callbacks change it.
|
|
411
|
+
const state = {
|
|
412
|
+
waiting: [],
|
|
413
|
+
waitingBytes: 0,
|
|
414
|
+
loaded: 0,
|
|
415
|
+
sending: null,
|
|
416
|
+
failed: null,
|
|
417
|
+
};
|
|
418
|
+
const send = () => {
|
|
419
|
+
const piece = joinPieces(state.waiting, state.waitingBytes);
|
|
420
|
+
state.waiting = [];
|
|
421
|
+
state.waitingBytes = 0;
|
|
422
|
+
state.loaded += piece.byteLength;
|
|
423
|
+
state.sending = push({ kind: 'progress', data: bytesToBase64(piece), loaded: state.loaded, total })
|
|
424
|
+
.then(() => {
|
|
425
|
+
state.sending = null;
|
|
426
|
+
// What arrived meanwhile goes now, not with the next read: on
|
|
427
|
+
// a trickle that could be a long way off.
|
|
428
|
+
if (state.waitingBytes > 0)
|
|
429
|
+
send();
|
|
430
|
+
}, (err) => {
|
|
431
|
+
state.sending = null;
|
|
432
|
+
state.failed = { err };
|
|
433
|
+
controller.abort();
|
|
434
|
+
});
|
|
435
|
+
};
|
|
436
|
+
// The pushes in flight done, and a failed one thrown.
|
|
437
|
+
const settle = async () => {
|
|
438
|
+
while (state.sending)
|
|
439
|
+
await state.sending;
|
|
440
|
+
if (state.failed)
|
|
441
|
+
throw state.failed.err;
|
|
442
|
+
};
|
|
443
|
+
try {
|
|
444
|
+
for (;;) {
|
|
445
|
+
let result;
|
|
446
|
+
try {
|
|
447
|
+
result = await reader.read();
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
// Our own abort, for a failed push: that failure is the cause.
|
|
451
|
+
if (state.failed)
|
|
452
|
+
throw state.failed.err;
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
const { done, value } = result;
|
|
456
|
+
if (done)
|
|
457
|
+
break;
|
|
458
|
+
if (state.failed)
|
|
459
|
+
throw state.failed.err;
|
|
460
|
+
if (!value)
|
|
461
|
+
continue;
|
|
462
|
+
// A read bigger than a piece is split, so no message outgrows one.
|
|
463
|
+
for (let at = 0; at < value.byteLength; at += MAX_PIECE_BYTES) {
|
|
464
|
+
const part = value.subarray(at, at + MAX_PIECE_BYTES);
|
|
465
|
+
// A full piece waiting: it goes next, once the pushes ahead of
|
|
466
|
+
// it are done.
|
|
467
|
+
if (state.waitingBytes > 0 && state.waitingBytes + part.byteLength > MAX_PIECE_BYTES) {
|
|
468
|
+
await settle();
|
|
469
|
+
if (state.waitingBytes > 0)
|
|
470
|
+
send();
|
|
471
|
+
}
|
|
472
|
+
state.waiting.push(part);
|
|
473
|
+
state.waitingBytes += part.byteLength;
|
|
474
|
+
if (!state.sending)
|
|
475
|
+
send();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
await settle();
|
|
479
|
+
if (state.waitingBytes > 0) {
|
|
480
|
+
send();
|
|
481
|
+
await settle();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
try {
|
|
486
|
+
await reader.cancel();
|
|
487
|
+
}
|
|
488
|
+
catch { /* already torn down */ }
|
|
489
|
+
throw err;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Install one request's header rule. Returns the id to remove
|
|
493
|
+
// afterwards, or null if it couldn't be installed — the caller decides
|
|
494
|
+
// whether that is fatal (it is, when the script asked for the headers).
|
|
495
|
+
async function installHeaderRule(spec) {
|
|
496
|
+
const dnr = chrome.declarativeNetRequest;
|
|
497
|
+
if (!dnr)
|
|
498
|
+
return null;
|
|
499
|
+
// The startup sweep reads every session rule and deletes the ones in
|
|
500
|
+
// our band. A rule added while that read is in flight is in its
|
|
501
|
+
// snapshot, so installing first would let the sweep delete a live
|
|
502
|
+
// rule. Wait it out — it runs once per worker life.
|
|
503
|
+
await whenStaleRulesCleared();
|
|
504
|
+
try {
|
|
505
|
+
// Clear the id first: `addRules` rejects a duplicate id outright,
|
|
506
|
+
// and an id can still be occupied if a rule ever escaped its
|
|
507
|
+
// `finally` (the counter wraps). Dropping a stale rule of ours is
|
|
508
|
+
// right anyway — its request is long gone.
|
|
509
|
+
await dnr.updateSessionRules({ removeRuleIds: [spec.id], addRules: [spec.rule] });
|
|
510
|
+
return spec.id;
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
console.info('[SourceMonkey] could not apply GM_xmlhttpRequest header rule', err);
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Take one request's rule back off. A rule that survives its request
|
|
518
|
+
// matches nothing *this* worker will request — the counter has moved
|
|
519
|
+
// past its tag — but the tag comes back when the next worker restarts
|
|
520
|
+
// the counter, so it is worth a second attempt before giving up and
|
|
521
|
+
// saying so. What catches the leftover is the startup sweep and the
|
|
522
|
+
// id pre-clear in `installHeaderRule`.
|
|
523
|
+
async function removeHeaderRule(ruleId) {
|
|
524
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
525
|
+
try {
|
|
526
|
+
await chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: [ruleId] });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
if (attempt === 1) {
|
|
531
|
+
console.info('[SourceMonkey] could not remove a GM_xmlhttpRequest header rule; it stays live until the next startup sweep', err);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Make a request abortable before `runXhr` starts it: while it waits
|
|
537
|
+
// on the user's answer to the connection prompt, the script may call
|
|
538
|
+
// `abort()` or its tab may close. `runXhr` takes the entry over; a
|
|
539
|
+
// caller that ends up not running the request calls `releaseXhr`.
|
|
540
|
+
export function reserveXhr(id, tabId) {
|
|
541
|
+
const controller = new AbortController();
|
|
542
|
+
activeXhrs.set(id, { controller, tabId });
|
|
543
|
+
if (earlyAborts.delete(id))
|
|
544
|
+
controller.abort();
|
|
545
|
+
return controller.signal;
|
|
546
|
+
}
|
|
547
|
+
// Ids aborted before the worker took their request: an `abort()` right
|
|
548
|
+
// after the call can overtake the start message, which first waits on
|
|
549
|
+
// checks (whose bridge it came through) before `reserveXhr`. Kept a
|
|
550
|
+
// minute, far longer than that wait; without them the request would
|
|
551
|
+
// still go out.
|
|
552
|
+
const earlyAborts = new Set();
|
|
553
|
+
const EARLY_ABORT_TTL_MS = 60 * 1000;
|
|
554
|
+
export function releaseXhr(id) {
|
|
555
|
+
activeXhrs.delete(id);
|
|
556
|
+
}
|
|
557
|
+
// Cancel an in-flight request by id. Idempotent — calling abort on a
|
|
558
|
+
// completed request is a no-op (the entry is gone). An id not seen yet
|
|
559
|
+
// is remembered, for a start message still on its way.
|
|
560
|
+
export function abortXhr(id) {
|
|
561
|
+
const entry = activeXhrs.get(id);
|
|
562
|
+
if (!entry) {
|
|
563
|
+
earlyAborts.add(id);
|
|
564
|
+
setTimeout(() => earlyAborts.delete(id), EARLY_ABORT_TTL_MS);
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
entry.controller.abort();
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
// Cancel everything a closed tab left running. Its page is gone, so no
|
|
571
|
+
// terminal event can be delivered and nobody is waiting for the body —
|
|
572
|
+
// but without this the fetch runs to completion in the worker, holding
|
|
573
|
+
// its header rule live for as long as the server keeps the connection
|
|
574
|
+
// open.
|
|
575
|
+
//
|
|
576
|
+
// Safe to iterate while aborting: the entry is removed in `runXhr`'s
|
|
577
|
+
// `finally`, which runs in a later microtask, and a Map iterator
|
|
578
|
+
// tolerates deletion regardless.
|
|
579
|
+
export function abortXhrsForTab(tabId) {
|
|
580
|
+
for (const entry of activeXhrs.values()) {
|
|
581
|
+
if (entry.tabId === tabId)
|
|
582
|
+
entry.controller.abort();
|
|
583
|
+
}
|
|
584
|
+
}
|