diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7c2ec069..ae1026ba 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,6 +19,10 @@ concurrency:
permissions:
contents: read
+env:
+ # cargo-generate-rpm 0.21.0 has prebuilts for both x86_64 and aarch64.
+ PACKAGING_TOOLS: cargo-deb@3.8.0,cargo-generate-rpm@0.21.0
+
jobs:
clippy_check:
name: Run clippy check
@@ -34,15 +38,10 @@ jobs:
components: clippy
- name: Cache cargo + target
- uses: actions/cache@v4
+ uses: Swatinem/rust-cache@v2
with:
- key: clippy-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- restore-keys: |
- clippy-${{ runner.os }}-
- path: |
- ~/.cargo/registry
- ~/.cargo/git
- ./target
+ workspaces: . -> target
+ key: clippy
# Frontend dist/ must exist for the server's include_dir! to compile.
# Reuses the artifact produced by the `build_frontend` job.
@@ -58,6 +57,7 @@ jobs:
rustfmt:
name: Check style
+ needs: [image_meta]
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -69,17 +69,37 @@ jobs:
components: rustfmt
- name: Run cargo fmt
- uses: actions-rs/cargo@v1
- with:
- command: fmt
- args: --all -- --check
+ run: cargo fmt --all -- --check
image_meta:
- name: Compute image tag
+ name: Validate version and compute image tag
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.vars.outputs.image_tag }}
image_name: ${{ steps.vars.outputs.image_name }}
+ package_version: ${{ steps.version.outputs.package_version }}
steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Validate release version
+ id: version
+ run: |
+ python3 - <<'PY'
+ import os
+ import tomllib
+
+ with open("privaxy/Cargo.toml", "rb") as manifest:
+ version = tomllib.load(manifest)["package"]["version"]
+ ref = os.environ["GITHUB_REF"]
+ if ref.startswith("refs/tags/v"):
+ tag_version = ref.removeprefix("refs/tags/v")
+ if tag_version != version:
+ print(f"::error::Release tag v{tag_version} does not match privaxy/Cargo.toml version {version}")
+ raise SystemExit(1)
+ with open(os.environ["GITHUB_OUTPUT"], "a") as output:
+ print(f"package_version={version}", file=output)
+ PY
+
- name: Get image tag
id: vars
run: |
@@ -135,6 +155,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
+ if: github.event_name == 'push'
uses: docker/login-action@v3
with:
registry: ghcr.io
@@ -147,7 +168,7 @@ jobs:
with:
images: ${{ needs.image_meta.outputs.image_name }}
- - name: Build and push by digest
+ - name: Build image
id: build
uses: docker/build-push-action@v6
with:
@@ -161,15 +182,18 @@ jobs:
annotations: ${{ steps.meta.outputs.annotations }}
cache-from: type=gha,scope=${{ matrix.cache_scope }}
cache-to: type=gha,mode=max,scope=${{ matrix.cache_scope }}
- outputs: type=image,name=${{ needs.image_meta.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
+ # PRs validate the image build without publishing to the registry.
+ outputs: ${{ github.event_name == 'push' && format('type=image,name={0},push-by-digest=true,name-canonical=true,push=true', needs.image_meta.outputs.image_name) || 'type=cacheonly' }}
- name: Export digest
+ if: github.event_name == 'push'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
+ if: github.event_name == 'push'
uses: actions/upload-artifact@v4
with:
name: image-digest-${{ matrix.cache_scope }}
@@ -180,6 +204,7 @@ jobs:
image_merge:
name: Merge multi-arch manifest
needs: [image_meta, image_build]
+ if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: read
@@ -237,7 +262,7 @@ jobs:
- name: Cache cargo + wasm target
uses: Swatinem/rust-cache@v2
with:
- workspaces: web_frontend -> target
+ workspaces: . -> target
shared-key: build-frontend
- name: Install node
@@ -315,19 +340,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- - name: Cache build artifacts
- uses: actions/cache@v3
- with:
- key: ${{ matrix.os }}-${{ matrix.target }}-artifacts
- path: |
- ./target
- ~/.cargo
-
- name: Install rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
+ - name: Cache build artifacts
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: . -> target
+ key: native-${{ matrix.target }}
+
- name: Install cross build dependencies
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross
@@ -352,14 +375,21 @@ jobs:
CC_x86_64_unknown_linux_musl: musl-gcc
CC_aarch64_unknown_linux_musl: musl-gcc
run: cargo build --release --target ${{ matrix.target }} --bin privaxy --target-dir target
+
+ - name: Install packaging tools
+ if: ${{ !endsWith(matrix.target, '-musl') }}
+ uses: taiki-e/install-action@v2
+ with:
+ tool: ${{ env.PACKAGING_TOOLS }}
+
- name: Build packages
if: ${{ !endsWith(matrix.target, '-musl') }}
run: |
# target/ is cached; discard packages from previous versions.
rm -f target/${{ matrix.target }}/release/privaxy_*.deb \
target/${{ matrix.target }}/release/privaxy-*.rpm
- cargo install cargo-deb && cargo deb -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
- cargo install cargo-generate-rpm && cargo generate-rpm -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
+ cargo deb --no-build -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
+ cargo generate-rpm -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
- uses: actions/upload-artifact@v4
if: ${{ !endsWith(matrix.target, '-musl') }}
with:
@@ -401,17 +431,15 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- - name: Cache build artifacts
- uses: actions/cache@v3
- with:
- key: ${{ matrix.os }}-${{ matrix.target }}-artifacts
- path: |
- ./target
- ~/.cargo
-
- name: Install rust
uses: dtolnay/rust-toolchain@nightly
+ - name: Cache build artifacts
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: . -> target
+ key: mips-${{ matrix.target }}-${{ hashFiles('Cross.toml') }}
+
- name: Install cross
uses: taiki-e/install-action@v2
with:
@@ -432,6 +460,12 @@ jobs:
- name: Build server
run: cross build --release --target ${{ matrix.target }} --bin privaxy --target-dir target
+ - name: Install packaging tools
+ if: ${{ endsWith(matrix.target, '-linux-gnu') }}
+ uses: taiki-e/install-action@v2
+ with:
+ tool: ${{ env.PACKAGING_TOOLS }}
+
- name: Build packages
if: ${{ endsWith(matrix.target, '-linux-gnu') }}
run: |
@@ -439,8 +473,8 @@ jobs:
rm -f target/${{ matrix.target }}/release/privaxy_*.deb \
target/${{ matrix.target }}/release/privaxy-*.rpm
variant=$(echo "${{ matrix.target }}" | cut -d- -f1)
- cargo install cargo-deb && cargo deb --no-build --no-strip --variant "$variant" -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
- cargo install cargo-generate-rpm && cargo generate-rpm -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
+ cargo deb --no-build --no-strip --variant "$variant" -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
+ cargo generate-rpm -p privaxy --target ${{ matrix.target }} -o target/${{ matrix.target }}/release
- uses: actions/upload-artifact@v4
if: ${{ endsWith(matrix.target, '-linux-gnu') }}
@@ -462,7 +496,7 @@ jobs:
release:
name: Create GitHub Release
- needs: [ci, ci_mips, image_merge]
+ needs: [image_meta, ci, ci_mips, image_merge]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
@@ -477,10 +511,9 @@ jobs:
path: ./artifacts
pattern: privaxy-*
merge-multiple: false
- - name: Set package version from tag
- run: |
- echo "PKG_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Flatten artifacts for release
+ env:
+ PKG_VERSION: ${{ needs.image_meta.outputs.package_version }}
run: |
mkdir ./release-assets
for dir in ./artifacts/privaxy-*/; do
diff --git a/.gitignore b/.gitignore
index 8ca84374..1da54055 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,7 @@ filters/__pycache__
node_modules
.vscode
**/target/*
-testing/
\ No newline at end of file
+testing/
+
+# Local opt-in request history can contain browsing metadata.
+statistics.redb
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c09ce61c..48a1a690 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,115 @@
# Changelog
-## Unreleased
+## v0.8.1
+
+- Use the combined uBlock Origin CDN lists for Ads, Badware risks, Privacy, Quick
+ fixes, and Unbreak. Migrate existing subscriptions without changing enabled
+ states, and retain an existing CDN subscription when an old source duplicates it.
+- Update bundled uBO scriptlets to 1.76.2, including JSON-edit and newer video
+ response hooks. Preserve shared dependencies and all scriptlet arguments,
+ isolate helpers from page globals, and honor trusted-scriptlet permissions.
+- Evaluate nested filter-list conditions before rule parsing using a portable
+ capability profile. Expand active relative `!#include` sub-lists, including the
+ uBO-specific rules in EasyList Cookie and Fanboy's Annoyance. Bound downloads,
+ reject cycles and paths outside the permitted source, and preserve the last
+ complete cache if a sub-list fails. Report malformed directives and unknown
+ conditions; log loaded-rule counts and reasons for unsupported or invalid rules.
+- Add a shared footer with the running server version linked to the project.
+ Check for stable releases automatically when opened and hourly thereafter,
+ showing an "Update available" release link only when a newer version is found.
+ Update checks are authenticated and time-limited, with successful checks cached
+ for one hour and failures for one minute.
+- Added opt-in persistent statistics and individual request history using redb.
+ History includes time ranges, trends, top domains/clients, and pagination, with
+ privacy levels that remove identifiers before collection. Query strings, URL
+ credentials, headers, and bodies are never saved in statistics.
+- Add an Endpoint column to Dashboard request history with the path separate
+ from the domain, visual truncation for long values, and the full path on hover.
+ Store paths without the first slash and restore it for display. Migrate existing
+ endpoint values to compact paths when upgrading the statistics database.
+ Endpoints follow domain privacy settings; older saved records remain readable.
+- Deduplicate domains, endpoints, and clients in shared lookup tables, with
+ compact ID references in each request. Upgrade existing history atomically to
+ storage version 3 and compact it. Remove unused values when requests expire;
+ privacy changes remove the corresponding values and lookup indexes.
+- Add client names in Statistics settings, shown in both dashboard layouts and
+ request history with the IP available on hover. Renaming preserves counts and
+ updates existing history; client privacy also hides names.
+- Added Statistics settings for retention, a request-count limit, clearing saved
+ history, and excluding domain patterns or client IPs from statistics and the live
+ Requests log. Exclusions do not change filtering behavior and work even when
+ persistence is disabled.
+- Bounded statistics buffering, batched transactional writes, visible storage
+ errors, and shutdown flushing keep storage failures off the proxy request path.
+ Existing installations retain memory-only statistics until explicitly enabled.
+- Show saved statistics directly on the Dashboard when persistence is enabled.
+ With persistence off or Anonymous mode selected, keep the original live,
+ per-process dashboard. Remove the separate History tab and redirect old
+ `/history` bookmarks to `/`. Both layouts retain the blocking toggle and CA
+ certificate download.
+- Live dashboard top blocked entries now use domains and select the top 50 after
+ sorting; client tracking is bounded.
+- Count each modified HTML response once when both head scripts and cosmetic
+ styles are injected, instead of counting the two injection stages separately.
+- Use TLS ClientHello SNI for IP-address CONNECT requests when choosing
+ interception certificates, applying hostname inclusions and exclusions, and
+ verifying upstream TLS. HTTP requests and WebSocket upgrades still connect to
+ the original CONNECT IP and port. Named CONNECT requests retain their existing
+ identity; DoH redirects and userscript fetches keep their own routing.
+- Tunnel CONNECT traffic to port 853 without interception, preserving end-to-end
+ DNS-over-TLS. The DoH policy continues to apply only to HTTP traffic. Explicit
+ CONNECT-host/IP exclusions bypass protocol probing.
+- Bound CONNECT protocol detection, ClientHello reading, and intercepted TLS
+ handshakes to 10 seconds each, with a 64 KiB ClientHello limit. Reject malformed,
+ truncated, oversized, or stalled handshakes while preserving fragmented TLS
+ records and buffered bytes, including when SNI selects a bypass tunnel.
+- Increase the upstream HTTP/2 response header limit from 16 KiB to 64 KiB and
+ allow 64 KiB of headers on intercepted HTTP/2 requests. This fixes 502 stream
+ protocol errors from responses with large CSP or Set-Cookie headers.
+- Advertise HTTP/2 and HTTP/1.1 through ALPN on intercepted TLS connections so
+ browsers can multiplex page resources. Enable adaptive HTTP/2 flow control on
+ those connections and TCP_NODELAY on accepted proxy sockets, tunnel sockets,
+ and upstream upgrade connections to reduce delays for small writes.
+- Preserve known-empty request bodies so the upstream client can retry HTTP/2
+ REFUSED_STREAM and graceful GOAWAY failures. Nonempty or unknown-length bodies
+ continue to stream without buffering or replay.
+- Include underlying DNS, TLS, socket, and HTTP/2 failure causes in proxy 502
+ pages and logs instead of only reporting "error sending request".
+- Improve live statistics, Requests, and log streams with visible connection
+ status, automatic reconnection, and handling for malformed or unexpected
+ frames. Leaving a live view cancels its connection and reconnect attempts.
+- Show retryable loading errors in settings, filter catalogs, API-key loading,
+ and userscript management. A failed sign-in status check now offers a retry
+ instead of incorrectly showing the login form.
+- Fix settings save feedback: acknowledge only successfully submitted values,
+ preserve text drafts after failures and edits made during a save, and disable
+ unchanged Save buttons and duplicate submissions. Ignore stale responses from
+ previously visited text-settings pages, and preserve pending exclusion edits
+ without restoring previously removed hosts.
+- Keep blocking controls at their confirmed state after failed updates, and
+ restore the confirmed Debug settings after failed automatic saves. Lock
+ Network and PAC fields while saving so later edits cannot be marked saved by
+ an earlier response.
+- Refresh installed filter lists after additions and removals while preserving
+ pending selections. Reset catalog pagination when searching, bound Next to
+ matching results, and keep failed removals visible. Built-in lists cannot be
+ removed through the catalog.
+- Make navigation, settings forms, and dialogs fit phone screens. Improve form
+ labels and error/status announcements, and wrap notification text instead of
+ truncating it.
+- Include [RELEASE.md](RELEASE.md) in generated GitHub release notes and discard cached
+ Debian/RPM packages before packaging to avoid publishing stale versions.
+- Keep Docker builds on pull requests while restricting image and manifest
+ publishing to pushes. Validate release tags against the server package version
+ before building, and use that version in release artifact names.
+- Correct the frontend cache path and key Rust caches by toolchain, dependencies,
+ and target. Use pinned prebuilt packaging tools and reuse compiled binaries
+ when creating Debian packages.
## v0.8.0 - 2026-10-02
-Release highlights and upgrade notes are in [RELEASE.md](RELEASE.md).
+Release highlights and upgrade notes are in the
+[v0.8.0 release notes](https://github.com/joshrmcdaniel/privaxy/blob/v0.8.0/RELEASE.md).
- Support both plain HTTP and HTTPS inside CONNECT tunnels, allowing compatible
HTTP proxies and adapters to chain into Privaxy. Detect HTTP versus TLS on any
diff --git a/Cargo.lock b/Cargo.lock
index 0ffb544c..27ae3b84 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2162,7 +2162,7 @@ dependencies = [
[[package]]
name = "privaxy"
-version = "0.8.0"
+version = "0.8.1"
dependencies = [
"adblock",
"argon2",
@@ -2176,6 +2176,7 @@ dependencies = [
"futures",
"futures-util",
"getrandom 0.3.4",
+ "h2",
"hex",
"hmac",
"http 1.4.2",
@@ -2192,9 +2193,11 @@ dependencies = [
"once_cell",
"openssl",
"portable-atomic",
+ "redb",
"regex",
"reqwest",
"rustls",
+ "semver",
"serde",
"serde-tuple-vec-map",
"serde_json",
@@ -2208,6 +2211,7 @@ dependencies = [
"tokio-stream",
"tokio-util",
"toml",
+ "tower-service",
"uluru",
"url",
"warp",
@@ -2393,6 +2397,15 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "redb"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de6c3b63e007e90ce536ec2ae4690826136a20ec8dbbbb400daef1bb999d2e36"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -3657,6 +3670,7 @@ dependencies = [
"gloo-net 0.6.0",
"gloo-timers 0.4.0",
"gloo-utils 0.3.0",
+ "js-sys",
"log",
"num-format",
"readonly",
diff --git a/Cross.toml b/Cross.toml
index e01eb019..cbfed87c 100644
--- a/Cross.toml
+++ b/Cross.toml
@@ -6,10 +6,6 @@ build-std = true
[target.mipsel-unknown-linux-musl]
build-std = true
-[target.mipsel-unknown-linux-musl.env]
-RUSTFLAGS = "-C target-feature=+crt-static"
[target.mips-unknown-linux-musl]
build-std = true
-[target.mips-unknown-linux-musl.env]
-RUSTFLAGS = "-C target-feature=+crt-static"
diff --git a/README.md b/README.md
index 8b1315b3..c8f5ada4 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ features, dependency updates, an improved UI, and server-friendly configuration
- [Cert-pinned hosts (exclusions)](#4-cert-pinned-hosts-exclusions)
- [Filter only selected hosts (inclusions)](#5-filter-only-selected-hosts-inclusions)
- [HTTP CONNECT and proxy chaining](#6-http-connect-and-proxy-chaining)
+ - [Persistent statistics and privacy](#7-persistent-statistics-and-privacy)
- [Screenshots](#screenshots)
- [Acknowledgements](#acknowledgements)
@@ -73,6 +74,7 @@ blocker more than it strictly replaces one.
- Suppport for [Adblock Plus filters](https://adblockplus.org/filter-cheatsheet), such as [easylist](https://easylist.to/).
- Web graphical user interface with a statistics display as well as a live request explorer.
+- Opt-in persistent statistics and request history, with privacy levels, retention limits, and domain exclusions.
- Support for uBlock origin's `js` syntax.
- Support for uBlock origin's `redirect` syntax.
- Support for uBlock origin's scriptlets.
@@ -89,6 +91,34 @@ blocker more than it strictly replaces one.
- [filterlists.com](https://filterlists.com) integration
- Ability to add custom filters
+Privaxy uses the combined uBlock Origin CDN lists for Ads, Badware risks,
+Privacy, Quick fixes, and Unbreak. Existing source-list subscriptions migrate
+automatically, preserving their enabled state. An already configured CDN
+subscription takes precedence over its old source-list duplicate. Separate
+mobile and resource-abuse subscriptions are retained for existing choices,
+although their rules are also included in the combined Ads and Privacy lists.
+
+Filter-list `!#if`, `!#else`, and `!#endif` directives use a portable uBO syntax
+profile (`ext_ublock` enabled, browser-specific flags and unsupported capabilities
+disabled). This selects scriptlet fallbacks for `!cap_html_filtering`; proxy HTML
+injection does not implement uBO's HTML-filtering or `$replace` syntax. Unknown
+conditions retain both branches, matching uBO, and are reported in the logs.
+Active `!#include` directives load relative sub-lists from the same directory or
+its descendants, following uBO's subscription format. Includes are expanded
+before caching; inactive branches are not downloaded. Failed downloads,
+malformed conditions, cycles, and includes outside the permitted source leave
+the previous complete cache untouched. Downloads are limited to 64 files,
+16 include levels, and 64 MiB per subscription. Custom filters have no source URL
+and cannot use includes.
+
+Bundled scriptlets come from uBlock Origin 1.76.2. Scriptlets marked
+`requiresTrust` are available to the official uBO filter repositories and manually
+entered Custom filters, but not arbitrary remote subscriptions. Unsupported
+filter rules are counted by rejection reason alongside the number loaded. These
+warnings do not mean the whole list failed to load. Filter syntax and browser
+integration still differ from the uBO extension, so list compatibility does not
+guarantee identical results on every video site.
+
## Installation
You can either utilize the docker image, binary, or the deb avaiable in releases.
@@ -285,18 +315,43 @@ exclusions used by ordinary proxy clients. No additional Privaxy setting is
needed. This feature supports chains that feed into Privaxy; configuring an
upstream proxy for Privaxy's own outbound traffic is outside its scope.
-The preceding proxy should preserve the destination hostname in CONNECT, which
-Privaxy uses for certificates and domain matching. An IP-only CONNECT for a
-named HTTPS site does not recover the hostname from TLS SNI and can fail
-certificate validation. Clients must trust Privaxy's CA; certificate-pinned
-services still need exclusions. Excluded or unselected hosts are tunneled
-without protocol probing.
+For an IP CONNECT (including resolved destinations from sing-box/TUN clients),
+Privaxy reads TLS ClientHello SNI before choosing an interception certificate
+or applying hostname inclusions and exclusions. A DNS SNI hostname supplies
+the certificate's DNS SAN, certificate cache identity, HTTP filtering identity,
+and upstream TLS SNI/verification name. The upstream connection still goes to
+the original CONNECT IP and port. No sing-box DNS or LAN routing changes are
+needed. Named CONNECT requests retain their existing behavior.
+
+Without usable DNS SNI, Privaxy retains the CONNECT identity: an intercepted
+literal IP receives an IP SAN, which cannot authenticate a hidden DNS hostname.
+Encrypted ClientHello (ECH) does not reveal its inner hostname to Privaxy.
+Malformed, truncated, oversized (over 64 KiB), or stalled ClientHellos fail
+closed; protocol detection, ClientHello reading, and the intercepted TLS
+handshake each have a 10-second timeout. Fragmented ClientHellos and buffered
+bytes are preserved, including when SNI selects a raw tunnel.
+
+Clients must still trust the existing Privaxy CA; certificate-pinned services
+need exclusions. Explicit CONNECT-host/IP exclusions bypass protocol probing.
+For other IP CONNECTs, hostname rules (including include-only mode) are decided
+after reading SNI. Exclusions take precedence over inclusions.
+
+CONNECT to port **853** is an opaque TCP tunnel for DNS-over-TLS, including
+`9.9.9.9:853` with SNI `dns.quad9.net`. TLS stays end-to-end and decrypted DNS
+never enters the HTTP parser. Privaxy's DoH policy applies only to HTTP traffic;
+it does not filter DoT. Non-HTTP TLS on other ports requires an exclusion.
Filtering covers TCP HTTP/HTTPS. Handle UDP/QUIC and non-HTTP protocols in your
routing policy; HTTP/3 traffic allowed directly will bypass filtering.
Privaxy's client statistics identify the connecting proxy's address, so devices
sharing that address appear together.
+Local TLS regression tests can be run with
+`cargo test -p privaxy --lib proxy::mitm::connect_tests`. They create temporary
+trusted test CAs in memory and use loopback fixtures, with no production config
+or external services. The DoT exchange uses an unprivileged ephemeral port;
+port 853's automatic tunnel selection is tested separately.
+
#### Example: tun2proxy
[tun2proxy](https://github.com/tun2proxy/tun2proxy) is one adapter that can send
@@ -319,6 +374,114 @@ for those steps.
> `password_hash` value from the config file and restart. The web UI will
> force the setup flow again.
+### 7. Persistent statistics and privacy
+
+Enable collection under **Settings → Statistics** to show saved totals,
+hourly/daily trends, top domains/clients, and a paginated request log directly
+on the **Dashboard**. With persistence off or Anonymous mode selected, the
+Dashboard keeps its original layout with the current process's live counters.
+Old `/history` bookmarks redirect to the Dashboard.
+Persistence is **off by default**; enabling it starts collecting new activity,
+without importing the live Requests feed.
+
+History records timestamps, destination hostnames, endpoint paths, client IP
+addresses, and whether a request was blocked or successfully forwarded, according
+to the selected privacy level. The Endpoint column shows only the path, such as
+`/204`, beside the separate domain. The database saves `204`, without duplicating
+the domain or the first `/`; that separator is restored for display. Query
+strings, URL credentials, headers, cookies, and request/response bodies are never
+stored. Long paths are shortened visually; hover over one to see its full value.
+Older records without an endpoint show "Not recorded". Domains, endpoints, and
+clients each have a shared lookup table: repeated values are saved once and
+request rows reference their IDs. Unused values are removed when their last
+request expires. Existing history migrates automatically to storage version 3
+and is compacted, converting older hostname-prefixed endpoints to paths too.
+Earlier builds that only support versions 1 or 2 cannot read the upgraded
+history database.
+Raw CONNECT tunnels and requests that fail before being forwarded are not
+counted. Clients behind another proxy share that proxy's client address.
+
+The privacy levels follow [Pi-hole's approach](https://docs.pi-hole.net/ftldns/privacylevels/):
+
+| Level | Saved identifiers |
+| --- | --- |
+| Show domains and clients | Hostname, endpoint path, and client IP |
+| Hide domains | Client IP only |
+| Hide domains and clients (default) | Neither; request times and outcomes remain |
+| Anonymous | No new history; session counters only |
+
+Hiding domains also hides endpoints. Hidden identifiers are removed **before
+queueing or storage**, without retaining a reversible mapping or a hash. While
+collection is enabled, raising privacy
+also redacts identifiers in existing history, including when entering anonymous
+mode. Lowering privacy cannot restore them. These controls apply to statistics;
+the separate live Requests feed and diagnostic logs can still contain full URLs.
+
+**Exclude domains from statistics and request logs** skips matching new traffic
+in session counters, modified-response counts, saved history, and the live
+Requests feed, even with persistence disabled. Filtering and interception still
+apply. Use one case-insensitive hostname or wildcard pattern per line:
+`example.com` matches exactly; `*.example.com` matches its subdomains. Do not
+include schemes, ports, or paths. Existing history and diagnostic logs are
+unaffected.
+
+**Exclude clients from statistics and request logs** applies the same exclusions
+to one exact IPv4 or IPv6 address per line. This works with persistence off too;
+it does not bypass ad blocking or delete previously saved requests.
+
+**Client names** assigns friendly names to IP addresses. Names appear in both
+dashboard layouts and saved history wherever client addresses are visible;
+hover over a name to see its IP. Renaming updates existing history without
+changing counts or merging different clients. Names are saved in configuration,
+so clearing history keeps them. Privacy settings that hide clients also hide
+their names on the Dashboard.
+
+Defaults are **7 days** and **100,000 saved requests**. The oldest records expire
+when either limit is reached; valid limits are 1–365 days and 100–1,000,000
+requests. History totals and top lists describe retained requests, so they can
+decrease as older entries expire. Modified-response totals are separate hourly
+aggregates retained by age. Time windows include the current hour and preceding
+hours. **Clear saved history** removes the database and starts fresh if enabled.
+Turning collection off stops recording and closes the database; it retains the
+existing file until cleared or collection is re-enabled and retention runs.
+
+The equivalent configuration section is:
+
+```toml
+[statistics]
+enabled = false
+privacy = "hide_domains_and_clients" # full, hide_domains, hide_domains_and_clients, anonymous
+retention_days = 7
+max_entries = 100000
+excluded_domains = ["example.com", "*.example.com"]
+excluded_clients = ["192.0.2.10", "2001:db8::10"]
+
+[statistics.client_names]
+"192.0.2.20" = "Living room TV"
+```
+
+UI changes apply immediately; file edits also apply on SIGHUP. Data stays local
+in `statistics.redb` under `PRIVAXY_BASE_PATH` (`/etc/privaxy` by default, `/conf`
+in Docker). Keep `/conf` on a persistent Docker volume for history to survive
+container replacement. No new ports or external database service are required.
+On Unix, the database is restricted to its owner (`0600`). Storage is not
+encrypted, and redaction/deletion is not secure erasure of filesystem snapshots,
+backups, or recoverable disk pages.
+
+[redb](https://github.com/cberner/redb) transactions run on a dedicated worker,
+with a bounded queue and batches flushed every five seconds or 512 events.
+SIGTERM/SIGINT also flush accepted events; an abrupt kill can lose the pending
+batch. Expired rows are pruned every minute and free space is compacted hourly;
+the row limit bounds retained data, not an exact file size. Disk/write errors
+pause recording and appear in Statistics settings without stopping the proxy.
+Free space and save settings again to retry. Queue overflow drops statistics
+events and reports their count instead of delaying proxy requests.
+
+The authenticated APIs are `GET/PUT /api/settings/statistics` and
+`GET/DELETE /api/history`. History accepts `hours`, `limit` (1–200), `before`
+(the previous response's `next_before` cursor), and `blocked=true|false`.
+Pagination and the blocked filter affect request rows, not the window's totals.
+
## Screenshots
diff --git a/RELEASE.md b/RELEASE.md
index 785cd29e..48a6eaf4 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,75 +1,73 @@
-# Privaxy v0.8.0
+# v0.8.1
-Privaxy v0.8.0 adds userscripts, filtering for selected hosts, and broader HTTP
-CONNECT support. It also improves streaming performance, filter-list recovery,
-and the tools for diagnosing sites that break under interception.
+Privaxy v0.8.1 improves uBlock Origin filter-list compatibility, adds optional
+persistent statistics, and fixes HTTP/2 failures and page-loading delays.
## Highlights
-- **Userscripts managed from the web UI.** Install by URL or paste, edit source,
- enable or disable individual scripts, and check for updates. Supports common
- Greasemonkey/Tampermonkey metadata and APIs, persistent storage, resources,
- menu commands, and server-side requests controlled by each script's
- `@connect` rules. URL-installed scripts also check for updates automatically.
-- **Filter only selected hosts.** Inclusion mode intercepts the hosts you
- select and tunnels everything else. Exclusions always take precedence, and
- an empty inclusion list bypasses all hosts. The same policy applies to
- HTTP, HTTPS, and PAC files; existing configurations keep their previous mode.
-- **HTTP CONNECT and incoming proxy chaining.** Compatible HTTP proxies and
- adapters can send both plain HTTP and HTTPS through CONNECT to Privaxy,
- including on nonstandard ports. Traffic uses the existing filtering,
- injection, and inclusion/exclusion policies. The README includes tun2proxy
- as one example, with routing configured by the user.
-- **Better filter-list management.** Refresh all enabled lists on demand, see
- download failures and their causes, and edit or remove user-added lists.
- Empty or invalid caches are recovered or downloaded again, and atomic
- replacement prevents failed writes from truncating a working list.
-- **Easier interception troubleshooting.** Proxy error pages link to an
- exclusion confirmation page. The UI also lists recent TLS interception
- failures, with actions to exclude or ignore a host. A configurable GUI URL
- keeps these links usable behind reverse proxies and Docker NAT.
+- **Updated uBlock Origin filtering.** Use the combined official CDN lists for
+ Ads, Badware risks, Privacy, Quick fixes, and Unbreak. Bundled scriptlets are
+ updated to uBO 1.76.2, including newer JSON-edit and video-response hooks,
+ shared helpers, and trusted-scriptlet permissions.
+- **Conditional rules and sub-lists.** Evaluate nested filter-list conditions
+ and download active relative `!#include` files. This fixes updates for EasyList
+ Cookie and Fanboy's Annoyance and loads their uBO-specific rules. Failed
+ sub-list downloads preserve the previous complete cache. Logs now show how
+ many rules loaded and why individual rules were rejected.
+- **Persistent statistics on the Dashboard.** Optional history provides time
+ ranges, trends, top domains and clients, and paginated requests. Settings
+ control privacy, retention, storage limits, exclusions, and clearing history.
+ With persistence off or Anonymous mode selected, the Dashboard retains its
+ original live layout. Old `/history` bookmarks redirect to the Dashboard.
+ Request history shows paths separately from domains, truncating long values
+ with the full path available on hover. Paths are stored without the first `/`,
+ which is restored in the UI. Domains, endpoints, and clients are stored once
+ and shared across request records to reduce repeated data. Assign friendly
+ client names or exclude client IPs from statistics and the live Requests log
+ in Statistics settings; excluded clients still receive normal filtering.
+- **More reliable live views and settings.** Dashboard statistics, Requests,
+ and logs show connection status and reconnect automatically. Settings retain
+ unsaved edits after errors, report save results accurately, and fit smaller
+ screens more comfortably.
+- **Version and update status.** The footer links the running Privaxy version
+ to the project and automatically shows an update link when a newer stable
+ release is detected.
## Reliability and performance
-- HTML responses stream as they are rewritten, allowing browsers to start
- rendering before the complete document arrives. Bounded buffering limits
- memory use when downstream clients are slow.
-- Larger WebSocket buffers, a shared adblock engine, and fewer repeated
- cosmetic lookups reduce overhead. Filter updates replace the engine without
- pausing requests for the full rebuild.
-- The proxy waits for initial filter loading before accepting traffic. The
- web UI remains available during loading and reports list failures.
-- Invalid configuration edits retain the last working configuration. Reloads
- no longer race for the web UI listener, and bind failures retry.
-- Fixes cover filtering on upgraded requests, WebSocket/MMTLS handling,
- truncated upstream responses, replacement-resource decoding, IPv6
- certificates, and CSP compatibility for injected content.
-- PAC files are also served at `/wpad.dat`, and CIDR bypass rules now render
- valid subnet masks.
-- The HTTP/TLS stack and frontend dependencies have been updated. Docker
- examples include log rotation to limit disk usage.
+- Larger HTTP/2 header limits fix 502 protocol errors on responses with large
+ CSP or Set-Cookie headers. Intercepted TLS connections advertise HTTP/2 so
+ browsers can multiplex requests; adaptive flow control and TCP_NODELAY reduce
+ delays. Known-empty requests can recover from retryable HTTP/2 failures.
+- IP-address CONNECT requests use TLS ClientHello SNI for certificates,
+ hostname policy, and upstream TLS verification while retaining the original
+ destination IP. Protocol detection and TLS handshakes have bounded timeouts.
+ CONNECT traffic to port 853 tunnels DNS-over-TLS without interception.
+- Proxy error pages and logs include underlying DNS, TLS, socket, and HTTP/2
+ failure causes. HTML modification counts no longer double-count a response
+ that receives both scripts and cosmetic styles.
+- CI validates release tags against the package version, avoids stale package
+ assets, improves build caches, and keeps pull-request Docker checks separate
+ from image publishing.
## Upgrading and compatibility
-- Existing configuration files remain supported. For v0.7.1 configurations,
- inclusion mode defaults off and no userscripts are installed automatically.
-- If upgrading from a development build with userscripts, **reload open pages**
- after upgrading. Userscript endpoints now use tokens bound to each script
- and matched page; disabling a script or changing its match rules revokes
- access.
-- Installed userscripts run on matching pages for every client behind the
- proxy, in the page's main JavaScript world. This is a subset of browser
- userscript-manager functionality; see the changelog for supported APIs and
- known limitations. Requests to private networks remain disabled by default.
-- For incoming proxy chains, preserve destination hostnames in CONNECT so
- domain filtering and HTTPS certificates work correctly. Clients still need
- to trust Privaxy's CA. This release does not add configuration for routing
- Privaxy's outbound traffic through another upstream proxy. TUN setup,
- routing, and UDP/QUIC handling remain the user's responsibility.
-- To pin this release in Docker, use `ghcr.io/joshrmcdaniel/privaxy:0.8.0`.
- Existing containers need to be recreated to adopt the log-rotation options
- shown in the README.
+- Existing configurations remain supported. Persistent statistics stay off
+ until enabled in Settings. Endpoints follow domain privacy settings; query
+ strings, URL credentials, headers, and bodies are never saved in statistics.
+ Existing history migrates to statistics storage version 3 when opened;
+ earlier builds that only support versions 1 or 2 cannot read the upgraded
+ database. Client names stay in configuration when saved history is cleared.
+- Legacy subscriptions for the five updated uBO lists migrate automatically
+ while preserving enabled states. An existing CDN subscription takes
+ precedence over its old source-list duplicate. Refresh enabled lists and
+ reload open pages after upgrading to use the latest rules and scriptlets.
+- Filter syntax and browser integration still differ from the uBO extension.
+ Unsupported-rule warnings describe individual skipped rules, not failed
+ subscription downloads, and this update does not guarantee identical video
+ ad blocking on every site.
+- To pin this release in Docker, use `ghcr.io/joshrmcdaniel/privaxy:0.8.1`.
-See the [full changelog](https://github.com/joshrmcdaniel/privaxy/blob/v0.8.0/CHANGELOG.md)
-and [proxy-chaining setup](https://github.com/joshrmcdaniel/privaxy/blob/v0.8.0/README.md#6-http-connect-and-proxy-chaining)
+See the [full changelog](https://github.com/joshrmcdaniel/privaxy/blob/v0.8.1/CHANGELOG.md)
+and [setup documentation](https://github.com/joshrmcdaniel/privaxy/blob/v0.8.1/README.md#setup)
for details.
diff --git a/privaxy/Cargo.toml b/privaxy/Cargo.toml
index 4fae88e2..7158d144 100644
--- a/privaxy/Cargo.toml
+++ b/privaxy/Cargo.toml
@@ -1,8 +1,9 @@
[package]
name = "privaxy"
description = "Next generation tracker and advertisement blocker"
-version = "0.8.0"
+version = "0.8.1"
edition = "2021"
+repository = "https://github.com/joshrmcdaniel/privaxy"
authors = [
"Pierre Barre ",
"Josh McDaniel <80354972+joshrmcdaniel@users.noreply.github.com>"
@@ -64,10 +65,12 @@ hyper-util = { version = "0.1", features = [
http-body-util = "0.1"
tokio = { version = "1.22.0", features = ["full"] }
serde_json = "1.0.89"
+semver = "1"
toml = "1"
serde = { version = "1.0.148", features = ["derive"] }
tokio-util = { version = "0.7.4", features = ["full"] }
tokio-stream = "0.1"
+tower-service = "0.3"
bytes = "1"
socket2 = "0.5"
# default-features = false only to drop `single-thread`: the multi-thread
@@ -137,6 +140,13 @@ tera = { version = "1", default-features = false }
argon2 = { version = "0.5", features = ["std"] }
hmac = "0.12"
filterlists-api = { path = "../filterlists-api", features = ["reqwest"] }
+# redb 4.3+ unconditionally imports libc::flock64 on GNU/Linux, but libc
+# does not expose that type on MIPS. Keep the last compatible release until
+# upstream fixes the range-lock backend; 4.2 retains the v3 database format.
+redb = "=4.2.0"
+
+[dev-dependencies]
+h2 = "0.4"
# getrandom 0.3 (pulled in via warp -> tokio-tungstenite -> rand) selects a
# backend that calls the libc `getrandom()` wrapper on mips{,el}-unknown-linux-gnu,
diff --git a/privaxy/build-scriptlets.mjs b/privaxy/build-scriptlets.mjs
index f55b44a7..d7dcc861 100644
--- a/privaxy/build-scriptlets.mjs
+++ b/privaxy/build-scriptlets.mjs
@@ -1,81 +1,40 @@
-// Compiles uBlock Origin's modern `scriptlets.js` (the
-// `builtinScriptlets.push({ name, fn, dependencies, ... })` format) into the
-// JSON Resource schema expected by adblock-rust's `Engine::use_resources`.
-//
-// Strategy lifted from brave/brave-core-crx-packager `lib/adBlockRustUtils.js`
-// (MPL-2.0): walk each scriptlet's transitive `dependencies`, inline the
-// dependency function source as a prelude, then wrap the scriptlet body in a
-// shim that exposes the legacy numbered template args `{{1}}`..`{{9}}`. The
-// `as_css` / numbered-arg style is what adblock-rust's templating layer
-// understands; the new format's positional `fn(...args)` calling convention
-// is bridged inside the shim.
-//
-// Invoked from build.rs as `node build-scriptlets.mjs `.
-
-import { promises as fs } from 'fs';
-import os from 'os';
-import path from 'path';
-import { pathToFileURL } from 'url';
+// Convert the pinned uBlock Origin modules into adblock-rust resources.
+// Keep functions and dependency helpers separate: the engine deduplicates them
+// and calls each scriptlet with its complete, safely quoted argument list.
+// Usage: node build-scriptlets.mjs
+import { promises as fs } from 'node:fs';
+import path from 'node:path';
+import { pathToFileURL } from 'node:url';
const [, , scriptletsPath, outPath] = process.argv;
if (!scriptletsPath || !outPath) {
- console.error('usage: node build-scriptlets.mjs ');
- process.exit(2);
+ throw new Error('usage: node build-scriptlets.mjs ');
}
-const wrap = (fnString, dependencyPrelude) => `{
- const args = ["{{1}}", "{{2}}", "{{3}}", "{{4}}", "{{5}}", "{{6}}", "{{7}}", "{{8}}", "{{9}}"];
- let last_arg_index = 0;
- for (const arg_index in args) {
- if (args[arg_index] === '{{' + (Number(arg_index) + 1) + '}}') {
- break;
- }
- last_arg_index += 1;
- }
- ${dependencyPrelude}
- (${fnString})(...args.slice(0, last_arg_index))
-}`;
-
-// scriptlets.js is plain ESM but lives under a `.js` extension in the vendor
-// dir. Node only treats `.js` as ESM with a sibling `package.json` declaring
-// `"type": "module"`, which we don't want to add. Copy to a `.mjs` in a
-// scratch dir so dynamic import resolves it unambiguously as ESM.
-const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'privaxy-scriptlets-'));
-const tmpFile = path.join(tmpDir, 'scriptlets.mjs');
-try {
- await fs.writeFile(tmpFile, await fs.readFile(scriptletsPath, 'utf8'));
- const { builtinScriptlets } = await import(pathToFileURL(tmpFile).href);
-
- const byName = new Map(builtinScriptlets.map((e) => [e.name, e]));
-
- const resources = builtinScriptlets
- // `.fn`-suffixed entries are pure dependency helpers (e.g. `safe-self.fn`),
- // not user-invocable scriptlets.
- .filter((s) => !s.name.endsWith('.fn'))
- .map((s) => {
- const deps = [...(s.dependencies ?? [])];
- for (const d of deps) {
- for (const rd of byName.get(d)?.dependencies ?? []) {
- if (!deps.includes(rd)) deps.push(rd);
- }
- }
- let prelude = '';
- // Reverse so leaf dependencies are declared before their consumers.
- for (const d of deps.reverse()) {
- const entry = byName.get(d);
- if (!entry) throw new Error(`Missing dependency: ${d} (referenced by ${s.name})`);
- prelude += entry.fn.toString() + '\n';
- }
- const content = Buffer.from(wrap(s.fn.toString(), prelude)).toString('base64');
- return {
- name: s.name,
- aliases: s.aliases ?? [],
- kind: { mime: 'application/javascript' },
- content,
- };
- });
-
- await fs.writeFile(outPath, JSON.stringify(resources));
-} finally {
- await fs.rm(tmpDir, { recursive: true, force: true });
+const { builtinScriptlets } = await import(pathToFileURL(path.resolve(scriptletsPath)).href);
+const byName = new Map(builtinScriptlets.map(entry => [entry.name, entry]));
+if (byName.size !== builtinScriptlets.length) {
+ throw new Error('Duplicate scriptlet resource name');
}
+
+const resources = builtinScriptlets.map(entry => {
+ const source = entry.fn.toString();
+ if (!entry.name.endsWith('.fn') && !/^function\s+\w+\s*\(/.test(source)) {
+ throw new Error(`Scriptlet ${entry.name} must be a named function`);
+ }
+ for (const dependency of entry.dependencies ?? []) {
+ if (!byName.has(dependency)) {
+ throw new Error(`Missing dependency ${dependency} for ${entry.name}`);
+ }
+ }
+ return {
+ name: entry.name,
+ aliases: entry.aliases ?? [],
+ kind: { mime: 'application/javascript' },
+ content: Buffer.from(source).toString('base64'),
+ dependencies: entry.dependencies ?? [],
+ // Must agree with TRUSTED_SCRIPTLETS in server/filter_list.rs.
+ permission: entry.requiresTrust ? 1 : 0,
+ };
+});
+await fs.writeFile(outPath, JSON.stringify(resources));
diff --git a/privaxy/build.rs b/privaxy/build.rs
index 3043b920..36eafc5b 100644
--- a/privaxy/build.rs
+++ b/privaxy/build.rs
@@ -132,7 +132,11 @@ fn build_scriptlet_resources(manifest_dir: &Path, out_dir: &Path) {
let builder = manifest_dir.join("build-scriptlets.mjs");
let out_path = out_dir.join("scriptlets-resources.json");
- println!("cargo:rerun-if-changed={}", scriptlets_src.display());
+ // The entry point imports the pinned modules and shared parser helpers.
+ println!(
+ "cargo:rerun-if-changed={}",
+ manifest_dir.join("src/resources/vendor/ublock").display()
+ );
println!("cargo:rerun-if-changed={}", builder.display());
// Allow cross-compile environments without Node (e.g. the cross-rs MIPS
diff --git a/privaxy/src/resources/vendor/ublock/README.md b/privaxy/src/resources/vendor/ublock/README.md
new file mode 100644
index 00000000..a93304e6
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/README.md
@@ -0,0 +1,28 @@
+# uBlock Origin resources
+
+The scriptlets are vendored from **uBlock Origin 1.76.2**, commit
+[`e25a1b3b9e0a9c8db345453f1b8a0a9d109668b7`](https://github.com/gorhill/uBlock/tree/e25a1b3b9e0a9c8db345453f1b8a0a9d109668b7).
+
+- `scriptlet-resources/` is an unmodified copy of upstream `src/js/resources/`.
+- `arglist-parser.js`, `jsonpath.js`, and `urlskip.js` are unmodified copies from
+ upstream `src/js/`, required by those modules.
+- `SCRIPTLETS-LICENSE.txt` is upstream's GPL-3.0 license. Individual source files
+ retain their copyright and license notices.
+- `scriptlets.js` is Privaxy's stable entry point; `package.json` enables ES modules
+ for Node during preprocessing. Neither file is injected into proxied pages.
+
+To update, copy these modules and dependencies from one pinned upstream commit,
+preserve the license, and update this provenance. From `privaxy/`, run:
+
+```sh
+mkdir -p prebuilt
+node build-scriptlets.mjs src/resources/vendor/ublock/scriptlets.js prebuilt/scriptlets-resources.json
+```
+
+The generated resource JSON is a build artifact. Normal Cargo builds generate it
+automatically; CI supplies it to cross-build containers without Node. The
+converter preserves dependency names and `requiresTrust` metadata and uses
+adblock-rust's function calling convention, including all filter arguments.
+
+The older `redirect-resources.js` and `web_accessible_resources/` remain separate
+redirect resources; they are not part of the scriptlet version pin above.
diff --git a/privaxy/src/resources/vendor/ublock/SCRIPTLETS-LICENSE.txt b/privaxy/src/resources/vendor/ublock/SCRIPTLETS-LICENSE.txt
new file mode 100644
index 00000000..5e1c8e21
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/SCRIPTLETS-LICENSE.txt
@@ -0,0 +1,674 @@
+GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. {http://fsf.org/}
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ {one line to give the program's name and a brief idea of what it does.}
+ Copyright (C) {year} {name of author}
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ {{project}} Copyright (C) {{year}} {{fullname}}
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+{http://www.gnu.org/licenses/}.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+{http://www.gnu.org/philosophy/why-not-lgpl.html}.
diff --git a/privaxy/src/resources/vendor/ublock/arglist-parser.js b/privaxy/src/resources/vendor/ublock/arglist-parser.js
new file mode 100644
index 00000000..708029e2
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/arglist-parser.js
@@ -0,0 +1,116 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2020-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/******************************************************************************/
+
+export class ArglistParser {
+ constructor(separatorChar = ',', mustQuote = false) {
+ this.separatorChar = this.actualSeparatorChar = separatorChar;
+ this.separatorCode = this.actualSeparatorCode = separatorChar.charCodeAt(0);
+ this.mustQuote = mustQuote;
+ this.quoteBeg = 0; this.quoteEnd = 0;
+ this.argBeg = 0; this.argEnd = 0;
+ this.separatorBeg = 0; this.separatorEnd = 0;
+ this.transform = false;
+ this.failed = false;
+ this.reWhitespaceStart = /^\s+/;
+ this.reWhitespaceEnd = /(?:^|\S)(\s+)$/;
+ this.reOddTrailingEscape = /(?:^|[^\\])(?:\\\\)*\\$/;
+ this.reTrailingEscapeChars = /\\+$/;
+ }
+ nextArg(pattern, beg = 0) {
+ const len = pattern.length;
+ this.quoteBeg = beg + this.leftWhitespaceCount(pattern.slice(beg));
+ this.failed = false;
+ const qc = pattern.charCodeAt(this.quoteBeg);
+ if ( qc === 0x22 /* " */ || qc === 0x27 /* ' */ || qc === 0x60 /* ` */ ) {
+ this.indexOfNextArgSeparator(pattern, qc);
+ if ( this.argEnd !== len ) {
+ this.quoteEnd = this.argEnd + 1;
+ this.separatorBeg = this.separatorEnd = this.quoteEnd;
+ this.separatorEnd += this.leftWhitespaceCount(pattern.slice(this.quoteEnd));
+ if ( this.separatorEnd === len ) { return this; }
+ if ( pattern.charCodeAt(this.separatorEnd) === this.separatorCode ) {
+ this.separatorEnd += 1;
+ return this;
+ }
+ }
+ }
+ this.indexOfNextArgSeparator(pattern, this.separatorCode);
+ this.separatorBeg = this.separatorEnd = this.argEnd;
+ if ( this.separatorBeg < len ) {
+ this.separatorEnd += 1;
+ }
+ this.argEnd -= this.rightWhitespaceCount(pattern.slice(0, this.separatorBeg));
+ this.quoteEnd = this.argEnd;
+ if ( this.mustQuote ) {
+ this.failed = true;
+ }
+ return this;
+ }
+ normalizeArg(s, char = '') {
+ if ( char === '' ) { char = this.actualSeparatorChar; }
+ let out = '';
+ let pos = 0;
+ while ( (pos = s.lastIndexOf(char)) !== -1 ) {
+ out = s.slice(pos) + out;
+ s = s.slice(0, pos);
+ const match = this.reTrailingEscapeChars.exec(s);
+ if ( match === null ) { continue; }
+ const tail = (match[0].length & 1) !== 0
+ ? match[0].slice(0, -1)
+ : match[0];
+ out = tail + out;
+ s = s.slice(0, -match[0].length);
+ }
+ if ( out === '' ) { return s; }
+ return s + out;
+ }
+ leftWhitespaceCount(s) {
+ const match = this.reWhitespaceStart.exec(s);
+ return match === null ? 0 : match[0].length;
+ }
+ rightWhitespaceCount(s) {
+ const match = this.reWhitespaceEnd.exec(s);
+ return match === null ? 0 : match[1].length;
+ }
+ indexOfNextArgSeparator(pattern, separatorCode) {
+ this.argBeg = this.argEnd = separatorCode !== this.separatorCode
+ ? this.quoteBeg + 1
+ : this.quoteBeg;
+ this.transform = false;
+ if ( separatorCode !== this.actualSeparatorCode ) {
+ this.actualSeparatorCode = separatorCode;
+ this.actualSeparatorChar = String.fromCharCode(separatorCode);
+ }
+ while ( this.argEnd < pattern.length ) {
+ const pos = pattern.indexOf(this.actualSeparatorChar, this.argEnd);
+ if ( pos === -1 ) {
+ return (this.argEnd = pattern.length);
+ }
+ if ( this.reOddTrailingEscape.test(pattern.slice(0, pos)) === false ) {
+ return (this.argEnd = pos);
+ }
+ this.transform = true;
+ this.argEnd = pos + 1;
+ }
+ }
+}
diff --git a/privaxy/src/resources/vendor/ublock/jsonpath.js b/privaxy/src/resources/vendor/ublock/jsonpath.js
new file mode 100644
index 00000000..bcdb90c5
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/jsonpath.js
@@ -0,0 +1,632 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2025-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+/**
+ * Implement the parsing of uBO-flavored JSON path queries.
+ *
+ * Reference to original JSON path syntax:
+ * https://www.rfc-editor.org/info/rfc9535/
+ *
+ * uBO-flavored JSON path implementation differs as follow:
+ * - Array slice operator not supported
+ * - Selectors can be a `/`-delimited regex, i.e. `$./pattern/`
+ * - Onle a single filter selector is allowed in filter selector syntax
+ *
+ * - uBO-flavored JSON path supports a limited set of filter selectors:
+ * set of expressions since unlike the official implementation, uBO can't use
+ * JS eval() to evaluate arbitrary JS expressions. The operand MUST be valid
+ * JSON. The currently supported expressions are:
+ * - ==: strict equality
+ * - !=: strict inequality
+ * - <: less than
+ * - <=: less than or equal to
+ * - >: greater than
+ * - >=: greater than or equal to
+ * - ^=: stringified value starts with
+ * - $=: stringified value ends with
+ * - *=: stringified value includes
+ * - =/.../: true if the regular expression matches the stringified value
+ *
+ * - uBO-flavored JSON path supports assignement operator:
+ * - =[any well-formed JSON value]
+ * - =repl(...): [to be documented] i.e. {"regex":"...","flags":"i","replacement":"..."}
+ * - =call(...): [to be documented] i.e. ["${obj}","setAttribute","${key}"]
+ * *
+ * - Examples (from "JSONPath examples" at reference link)
+ * - .store.book[*].author
+ * - ..author
+ * - .store.*
+ * - .store..price
+ * - ..book[2]
+ * - ..book[?(.isbn)]
+ * - ..book[?(.price<10)]
+ * - ..*
+ *
+ * uBO-flavored syntax supports assigning a value to a resolved JSON path by
+ * appending `=[value]` to the JSON path query. The assigned value MUST be
+ * valid JSON. Examples:
+ * - .store..price=0
+ * - .store.book[*].author="redacted"
+ *
+ * A JSONPath instance can be use to compile a JSON path query, and the result
+ * of the compilation can be applied to different objects. When a JSON path
+ * query does not assign a value, the resolved property will be removed.
+ *
+ * More capabilities can be added in the future as needed.
+ *
+ * */
+
+export class JSONPath {
+ static create(query) {
+ const jsonp = new JSONPath();
+ jsonp.compile(query);
+ return jsonp;
+ }
+ static toJSON(obj, stringifier, ...args) {
+ return (stringifier || JSON.stringify)(obj, ...args)
+ .replace(/\//g, '\\/');
+ }
+ static keys = Object.keys;
+ static entries = Object.entries;
+ static hasOwn = Object.hasOwn;
+ static Regex = RegExp;
+ get value() {
+ return this.#compiled && this.#compiled.rval;
+ }
+ set value(v) {
+ if ( this.#compiled === undefined ) { return; }
+ this.#compiled.rval = v;
+ }
+ get valid() {
+ return this.#compiled !== undefined;
+ }
+ compile(query) {
+ this.#compiled = undefined;
+ this.v2 = query.startsWith('v2:');
+ if ( this.v2 ) { query = query.slice(3); }
+ const r = this.#compile(query, 0);
+ if ( r === undefined ) { return; }
+ if ( r.i !== query.length ) {
+ let val;
+ if ( query.startsWith('=', r.i) ) {
+ const match = this.#reRval.exec(query.slice(r.i));
+ if ( match ) {
+ r.modify = match[1];
+ val = match[2];
+ } else {
+ val = query.slice(r.i+1);
+ }
+ } else if ( query.startsWith('+=', r.i) ) {
+ r.modify = '+';
+ val = query.slice(r.i+2);
+ }
+ try { r.rval = JSON.parse(val); }
+ catch { return; }
+ }
+ r.v2 = this.v2;
+ this.#compiled = r;
+ }
+ evaluate(root) {
+ if ( this.valid === false ) { return []; }
+ this.#root = { '$': root };
+ const paths = this.#evaluate(this.#compiled.steps, []);
+ this.#root = null;
+ return paths;
+ }
+ apply(root) {
+ if ( this.valid === false ) { return; }
+ const { rval } = this.#compiled;
+ this.#root = { '$': root };
+ const paths = this.#evaluate(this.#compiled.steps, []);
+ let i = paths.length
+ if ( i === 0 ) { this.#root = null; return; }
+ while ( i-- ) {
+ const { obj, key } = this.#resolvePath(paths[i]);
+ if ( obj === undefined ) { continue; }
+ if ( rval !== undefined ) {
+ this.#modifyVal(obj, key);
+ } else if ( Array.isArray(obj) && typeof key === 'number' ) {
+ obj.splice(key, 1);
+ } else {
+ delete obj[key];
+ }
+ }
+ const result = this.#root['$'] ?? null;
+ this.#root = null;
+ return result;
+ }
+ dump() {
+ return JSON.stringify(this.#compiled);
+ }
+ toJSON(obj, ...args) {
+ return JSONPath.toJSON(obj, null, ...args)
+ }
+ get [Symbol.toStringTag]() {
+ return 'JSONPath';
+ }
+ #UNDEFINED = 0;
+ #ROOT = 1;
+ #CURRENT = 2;
+ #CHILDREN = 3;
+ #DESCENDANTS = 4;
+ #QUANTIFIER = 5;
+ #reUnquotedIdentifier = /^[A-Za-z_][\w]*|^\*/;
+ #reExpr = /^\s*([!=^$*]=|[<>]=?)\s*(.+?)\]/;
+ #reIndice = /^-?\d+/;
+ #reRval = /^=([a-z]+)\((.+)\)$/;
+ #reQuantifier = /^\{(\d+|\d+,\d+|\d+,|,\d+)\};\$/;
+ #root;
+ #compiled;
+ #compile(query, i) {
+ if ( query.length === 0 ) { return; }
+ const steps = [];
+ let c = query.charCodeAt(i);
+ if ( c === 0x24 /* $ */ ) {
+ steps.push({ mv: this.#ROOT });
+ i += 1;
+ } else if ( c === 0x40 /* @ */ ) {
+ steps.push({ mv: this.#CURRENT });
+ i += 1;
+ } else {
+ steps.push({ mv: i === 0 ? this.#ROOT : this.#CURRENT });
+ }
+ let mv = this.#UNDEFINED;
+ for (;;) {
+ if ( i === query.length ) { break; }
+ c = query.charCodeAt(i);
+ if ( c === 0x20 /* whitespace */ ) {
+ i += 1;
+ continue;
+ }
+ // Dot accessor syntax
+ if ( c === 0x2E /* . */ ) {
+ if ( mv !== this.#UNDEFINED ) { return; }
+ if ( query.startsWith('..', i) ) {
+ mv = this.#DESCENDANTS;
+ i += 2;
+ } else {
+ mv = this.#CHILDREN;
+ i += 1;
+ }
+ continue;
+ }
+ if ( c === 0x3B /* ; */ ) {
+ if ( query.startsWith(';$', i) === false ) { return; }
+ steps.push(
+ { mv: this.#QUANTIFIER, min: 1, max: 1e6 },
+ { mv: this.#ROOT }
+ );
+ i += 2;
+ mv = this.#UNDEFINED;
+ continue;
+ }
+ if ( c === 0x7B /* { */ ) {
+ const match = this.#reQuantifier.exec(query.slice(i));
+ if ( match === null ) { return; }
+ const comma = match[1].indexOf(',');
+ let min, max;
+ if ( comma === -1 ) {
+ min = max = parseInt(match[1]);
+ } else {
+ min = parseInt(match[1].slice(0, comma)) || 0;
+ max = parseInt(match[1].slice(comma+1)) || 1e6;
+ }
+ steps.push(
+ { mv: this.#QUANTIFIER, min, max },
+ { mv: this.#ROOT }
+ );
+ i += match[0].length;
+ mv = this.#UNDEFINED;
+ continue;
+ }
+ if ( c !== 0x5B /* [ */ ) {
+ if ( mv === this.#UNDEFINED ) {
+ const step = steps.at(-1);
+ if ( step === undefined ) { return; }
+ const j = this.#compileExpr(query, step, i);
+ if ( j ) { i = j; }
+ break;
+ }
+ const r = this.#consumeUnquotedIdentifier(query, i);
+ if ( r === undefined ) { return; }
+ steps.push({ mv, k: r.s });
+ i = r.i;
+ mv = this.#UNDEFINED;
+ continue;
+ }
+ // Bracket accessor syntax
+ if ( mv === this.#CHILDREN ) { return; }
+ if ( query.startsWith('[?', i) ) {
+ const not = query.charCodeAt(i+2) === 0x21 /* ! */ ? 1 : 0;
+ const j = i + 2 + not;
+ const r = this.#compile(query, j);
+ if ( r === undefined ) { return; }
+ if ( query.startsWith(']', r.i) === false ) { return; }
+ if ( not ) { r.steps.at(-1).not = true; }
+ steps.push({ mv: mv || this.#CHILDREN, steps: r.steps });
+ i = r.i + 1;
+ mv = this.#UNDEFINED;
+ continue;
+ }
+ if ( query.startsWith('[*]', i) ) {
+ mv ||= this.#CHILDREN;
+ steps.push({ mv, k: '*' });
+ i += 3;
+ mv = this.#UNDEFINED;
+ continue;
+ }
+ const r = this.#consumeIdentifier(query, i+1);
+ if ( r === undefined ) { return; }
+ mv ||= this.#CHILDREN;
+ steps.push({ mv, k: r.s });
+ i = r.i + 1;
+ mv = this.#UNDEFINED;
+ }
+ if ( steps.length === 0 ) { return; }
+ if ( mv !== this.#UNDEFINED ) { return; }
+ return { steps, i };
+ }
+ #evaluate(steps, pathin) {
+ let resultset = [];
+ if ( Array.isArray(steps) === false ) { return resultset; }
+ for ( const step of steps ) {
+ switch ( step.mv ) {
+ case this.#ROOT:
+ resultset = [ [ '$' ] ];
+ break;
+ case this.#CURRENT:
+ if ( step.op ) {
+ const { obj, key } = this.#resolvePath(pathin);
+ if ( obj === undefined ) { return []; }
+ const outcome = this.#evaluateExpr(step, obj, key);
+ if ( outcome !== true ) { break; }
+ }
+ resultset = [ pathin ];
+ break;
+ case this.#CHILDREN:
+ case this.#DESCENDANTS: {
+ if ( resultset.length === 0 ) { break; }
+ resultset = this.#getMatches(resultset, step);
+ break;
+ }
+ case this.#QUANTIFIER: {
+ const { length } = resultset;
+ if ( length < step.min || length > step.max ) { return []; }
+ resultset = [];
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return resultset;
+ }
+ #getMatches(listin, step) {
+ const listout = [];
+ for ( const pathin of listin ) {
+ const { value: owner } = this.#resolvePath(pathin);
+ if ( owner === undefined ) { continue; }
+ if ( step.steps ) {
+ this.#getMatchesFromExpr(pathin, step, owner, listout);
+ continue;
+ }
+ const iter = this.#expandKey(owner, step.k);
+ if ( iter ) {
+ for ( const k of iter ) {
+ const outcome = this.#evaluateExpr(step, owner, k);
+ if ( outcome !== true ) { continue; }
+ listout.push([ ...pathin, k ]);
+ }
+ }
+ if ( step.mv !== this.#DESCENDANTS ) { continue; }
+ for ( const { obj, key, path } of this.#getDescendants(owner, true) ) {
+ const iter = this.#expandKey(obj[key], step.k);
+ if ( iter === undefined ) { continue; }
+ for ( const k of iter ) {
+ const outcome = this.#evaluateExpr(step, obj[key], k);
+ if ( outcome !== true ) { continue; }
+ listout.push([ ...pathin, ...path, k ]);
+ }
+ }
+ }
+ return listout;
+ }
+ #expandKey(owner, k) {
+ if ( typeof owner !== 'object' || owner === null ) { return; }
+ if ( Array.isArray(k) ) {
+ const out = [];
+ for ( const a of k ) {
+ const iter = this.#expandKey(owner, a);
+ if ( iter === undefined ) { continue; }
+ out.push(...iter);
+ }
+ return out;
+ }
+ if ( typeof k === 'number' ) {
+ if ( Array.isArray(owner) === false ) { return; }
+ return [ k >= 0 ? k : owner.length + k ];
+ }
+ if ( k === '*' ) {
+ if ( Array.isArray(owner) ) { return owner.keys(); }
+ return JSONPath.keys(owner);
+ }
+ if ( k instanceof JSONPath.Regex ) {
+ const out = [];
+ for ( const key of JSONPath.keys(owner) ) {
+ if ( k.test(key) === false ) { continue; }
+ out.push(key);
+ }
+ return out;
+ }
+ return [ k ];
+ }
+ #getMatchesFromExpr(pathin, step, owner, out) {
+ const recursive = step.mv === this.#DESCENDANTS;
+ const v2 = this.#compiled.v2 || recursive || Array.isArray(owner);
+ for ( const { path } of this.#getDescendants(owner, recursive) ) {
+ const q = v2 ? [ ...pathin, ...path ] : pathin;
+ const r = this.#evaluate(step.steps, q);
+ if ( Boolean(r?.length) === false ) { continue; }
+ out.push(q);
+ if ( v2 === false ) { break; }
+ }
+ }
+ #getDescendants(v, recursive) {
+ const iterator = {
+ next() {
+ const n = this.stack.length;
+ if ( n === 0 ) {
+ this.value = undefined;
+ this.done = true;
+ return this;
+ }
+ const details = this.stack[n-1];
+ const entry = details.keys.next();
+ if ( entry.done ) {
+ this.stack.pop();
+ this.path.pop();
+ return this.next();
+ }
+ this.path[n-1] = entry.value;
+ this.value = {
+ obj: details.obj,
+ key: entry.value,
+ path: this.path.slice(),
+ };
+ const v = this.value.obj[this.value.key];
+ if ( recursive && typeof v === 'object' && v !== null && this.seen.has(v) === false ) {
+ if ( Array.isArray(v) ) {
+ this.stack.push({ obj: v, keys: v.keys() });
+ } else {
+ this.stack.push({ obj: v, keys: JSONPath.keys(v).values() });
+ }
+ this.seen.add(v);
+ }
+ return this;
+ },
+ path: [],
+ value: undefined,
+ done: false,
+ stack: [],
+ seen: new WeakSet(),
+ [Symbol.iterator]() { return this; },
+ };
+ if ( Array.isArray(v) ) {
+ iterator.stack.push({ obj: v, keys: v.keys() });
+ } else if ( typeof v === 'object' && v !== null ) {
+ iterator.stack.push({ obj: v, keys: JSONPath.keys(v).values() });
+ }
+ return iterator;
+ }
+ #consumeIdentifier(query, i) {
+ const keys = [];
+ let needIdentifier = true;
+ while ( i < query.length ) {
+ const c0 = query.charCodeAt(i);
+ if ( c0 === 0x5D /* ] */ ) { break; }
+ if ( c0 === 0x20 /* SPACE */ ) {
+ i += 1;
+ continue;
+ }
+ if ( c0 === 0x2C /* , */ ) {
+ if ( needIdentifier ) { return; }
+ i += 1;
+ needIdentifier = true;
+ continue;
+ }
+ if ( c0 === 0x22 /* " */ || c0 === 0x27 /* ' */ ) {
+ const r = this.#untilChar(query, c0, i+1);
+ if ( r === undefined ) { return; }
+ keys.push(r.s);
+ i = r.i;
+ needIdentifier = false;
+ continue;
+ }
+ if ( c0 === 0x2D /* - */ || c0 >= 0x30 && c0 <= 0x39 ) {
+ const match = this.#reIndice.exec(query.slice(i));
+ if ( match === null ) { return; }
+ const indice = parseInt(query.slice(i), 10);
+ keys.push(indice);
+ i += match[0].length;
+ needIdentifier = false;
+ continue;
+ }
+ if ( this.v2 ) { return; }
+ const r = this.#consumeUnquotedIdentifier(query, i);
+ if ( r === undefined ) { return; }
+ keys.push(r.s);
+ i = r.i;
+ }
+ if ( needIdentifier ) { return; }
+ return { s: keys.length === 1 ? keys[0] : keys, i };
+ }
+ #consumeUnquotedIdentifier(query, i) {
+ if ( query.charCodeAt(i) === 0x2F /* / */ ) {
+ const r = this.#untilChar(query, 0x2F, i+1);
+ if ( r === undefined ) { return; }
+ let re;
+ try { re = new JSONPath.Regex(r.s); } catch { return; }
+ return { s: re, i: r.i };
+ }
+ const match = this.#reUnquotedIdentifier.exec(query.slice(i));
+ if ( match === null ) { return; }
+ return { s: match[0], i: i + match[0].length };
+ }
+ #untilChar(query, targetCharCode, i) {
+ const len = query.length;
+ const parts = [];
+ let beg = i, end = i;
+ for (;;) {
+ if ( end === len ) { return; }
+ const c = query.charCodeAt(end);
+ if ( c === targetCharCode ) {
+ parts.push(query.slice(beg, end));
+ end += 1;
+ break;
+ }
+ if ( c === 0x5C /* \ */ && (end+1) < len ) {
+ const d = query.charCodeAt(end+1);
+ if ( d === targetCharCode ) {
+ parts.push(query.slice(beg, end));
+ end += 1;
+ beg = end;
+ }
+ }
+ end += 1;
+ }
+ return { s: parts.join(''), i: end };
+ }
+ #compileExpr(query, step, i) {
+ if ( query.startsWith('=/', i) ) {
+ const r = this.#untilChar(query, 0x2F /* / */, i+2);
+ if ( r === undefined ) { return i; }
+ const match = /^[i]/.exec(query.slice(r.i));
+ try {
+ step.rval = new JSONPath.Regex(r.s, match && match[0] || undefined);
+ } catch { return; }
+ step.op = 're';
+ if ( match ) { r.i += match[0].length; }
+ return r.i;
+ }
+ const match = this.#reExpr.exec(query.slice(i));
+ if ( match === null ) { return; }
+ const op = match[1], rval = match[2];
+ if ( rval.charCodeAt(0) === 0x27 /* ' */ ) {
+ const r = this.#untilChar(rval, 0x27, 1);
+ if ( r === undefined ) { return; }
+ step.rval = r.s;
+ step.op = op;
+ } else {
+ try {
+ step.rval = JSON.parse(rval);
+ step.op = op;
+ } catch { return; }
+ }
+ return i + match[0].length - 1;
+ }
+ #resolvePath(path) {
+ if ( path.length === 0 ) { return { value: this.#root }; }
+ const key = path.at(-1);
+ let obj = this.#root
+ for ( let i = 0, n = path.length-1; i < n; i++ ) {
+ obj = obj[path[i]];
+ if ( obj instanceof Object === false ) { return {}; }
+ }
+ return { obj, key, value: obj[key] };
+ }
+ #evaluateExpr(step, owner, k) {
+ if ( owner === undefined || owner === null ) { return; }
+ const hasOwn = owner[k] !== undefined || JSONPath.hasOwn(owner, k);
+ if ( step.op !== undefined && hasOwn === false ) { return; }
+ const target = step.not !== true;
+ const v = owner[k];
+ switch ( step.op ) {
+ case '==': return (v === step.rval) === target;
+ case '!=': return (v !== step.rval) === target;
+ case '<': return (v < step.rval) === target;
+ case '<=': return (v <= step.rval) === target;
+ case '>': return (v > step.rval) === target;
+ case '>=': return (v >= step.rval) === target;
+ case '^=': return `${v}`.startsWith(step.rval) === target;
+ case '$=': return `${v}`.endsWith(step.rval) === target;
+ case '*=': return `${v}`.includes(step.rval) === target;
+ case 're': return step.rval.test(`${v}`);
+ default: break;
+ }
+ return hasOwn === target;
+ }
+ #modifyVal(obj, key) {
+ let { modify, rval } = this.#compiled;
+ if ( typeof rval === 'string' ) {
+ rval = rval.replace('${now}', `${Date.now()}`);
+ }
+ switch ( modify ) {
+ case undefined:
+ obj[key] = rval;
+ break;
+ case '+': {
+ if ( rval instanceof Object === false ) { return; }
+ const lval = obj[key];
+ if ( lval instanceof Object === false ) { return; }
+ if ( Array.isArray(lval) ) {
+ if ( Array.isArray(rval) ) {
+ lval.push(...rval); break;
+ }
+ lval.push(rval); break;
+ }
+ for ( const [ k, v ] of JSONPath.entries(rval) ) {
+ lval[k] = v;
+ }
+ break;
+ }
+ case 'call': {
+ const entries = rval.slice();
+ if ( entries.length < 2 ) { break; }
+ entries.forEach((a, i, aa) => {
+ if ( a === '${obj}' ) { aa[i] = obj; }
+ else if ( a === '${key}' ) { aa[i] = key; }
+ else if ( a === '${val}' ) { aa[i] = obj[key]; }
+ });
+ const instance = entries[0] ?? self;
+ instance[entries[1]](...entries.slice(2));
+ break;
+ }
+ case 'repl': {
+ const lval = obj[key];
+ if ( typeof lval !== 'string' ) { return; }
+ if ( this.#compiled.re === undefined ) {
+ this.#compiled.re = null;
+ try {
+ this.#compiled.re = rval.regex !== undefined
+ ? new JSONPath.Regex(rval.regex, rval.flags)
+ : new JSONPath.Regex(rval.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
+ } catch { }
+ }
+ if ( this.#compiled.re === null ) { return; }
+ obj[key] = lval.replace(this.#compiled.re, rval.replacement);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+}
diff --git a/privaxy/src/resources/vendor/ublock/package.json b/privaxy/src/resources/vendor/ublock/package.json
new file mode 100644
index 00000000..e986b24b
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/package.json
@@ -0,0 +1,4 @@
+{
+ "private": true,
+ "type": "module"
+}
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/abort-current-script.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/abort-current-script.js
new file mode 100644
index 00000000..c25ba072
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/abort-current-script.js
@@ -0,0 +1,123 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { getExceptionTokenFn, trapPropertyFn } from './utils.js';
+import { registerScriptlet } from './base.js';
+import { runAtHtmlElementFn } from './run-at.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+// Issues to mind before changing anything:
+// https://github.com/uBlockOrigin/uBlock-issues/issues/2154
+function abortCurrentScriptFn(
+ target = '',
+ needle = '',
+ context = ''
+) {
+ if ( typeof target !== 'string' ) { return; }
+ if ( target === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('abort-current-script', target, needle, context);
+ const reNeedle = safe.patternToRegex(needle);
+ const reContext = safe.patternToRegex(context);
+ const thisScript = document.currentScript;
+ const exceptionToken = getExceptionTokenFn();
+ const scriptTexts = new WeakMap();
+ const textContentGetter = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent').get;
+ const getScriptText = elem => {
+ let text = textContentGetter.call(elem);
+ if ( text.trim() !== '' ) { return text; }
+ if ( scriptTexts.has(elem) ) { return scriptTexts.get(elem); }
+ const [ , mime, content ] = /^data:([^,]*),(.+)$/.exec(elem.src.trim()) ||
+ [ '', '', '' ];
+ try {
+ switch ( true ) {
+ case mime.endsWith(';base64'):
+ text = self.atob(content);
+ break;
+ default:
+ text = self.decodeURIComponent(content);
+ break;
+ }
+ } catch {
+ }
+ scriptTexts.set(elem, text);
+ return text;
+ };
+ const validate = ( ) => {
+ const e = document.currentScript;
+ if ( e instanceof HTMLScriptElement === false ) { return; }
+ if ( e === thisScript ) { return; }
+ if ( context !== '' && reContext.test(e.src) === false ) { return; }
+ if ( safe.logLevel > 1 && context !== '' ) {
+ safe.uboLog(logPrefix, `Matched src\n${e.src}`);
+ }
+ const scriptText = getScriptText(e);
+ if ( reNeedle.test(scriptText) === false ) { return; }
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Matched text\n${scriptText}`);
+ }
+ safe.uboLog(logPrefix, 'Aborted');
+ throw new ReferenceError(exceptionToken);
+ };
+ let currentValue = trapPropertyFn(target, {
+ get: function() {
+ validate();
+ return currentValue;
+ },
+ set: function(a) {
+ validate();
+ currentValue = a;
+ }
+ }, { canThrow: true });
+}
+registerScriptlet(abortCurrentScriptFn , {
+ name: 'abort-current-script.fn',
+ dependencies: [
+ getExceptionTokenFn,
+ safeSelf,
+ trapPropertyFn,
+ ],
+});
+
+/******************************************************************************/
+
+// Issues to mind before changing anything:
+// https://github.com/uBlockOrigin/uBlock-issues/issues/2154
+export function abortCurrentScript(...args) {
+ runAtHtmlElementFn(( ) => {
+ abortCurrentScriptFn(...args);
+ });
+}
+registerScriptlet(abortCurrentScript , {
+ name: 'abort-current-script.js',
+ aliases: [
+ 'abort-current-inline-script.js',
+ 'acis.js',
+ 'acs.js',
+ ],
+ dependencies: [
+ abortCurrentScriptFn,
+ runAtHtmlElementFn,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/attribute.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/attribute.js
new file mode 100644
index 00000000..b7052790
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/attribute.js
@@ -0,0 +1,337 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { offIdleFn, onIdleFn } from './utils.js';
+import { registerScriptlet } from './base.js';
+import { runAt } from './run-at.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+export function setAttrFn(
+ trusted = false,
+ logPrefix,
+ selector = '',
+ attr = '',
+ value = '',
+ options = {}
+) {
+ if ( selector === '' ) { return; }
+ if ( attr === '' ) { return; }
+
+ const safe = safeSelf();
+ const copyFrom = trusted === false && /^\[.+\]$/.test(value)
+ ? value.slice(1, -1)
+ : '';
+
+ const extractValue = elem => copyFrom !== ''
+ ? elem.getAttribute(copyFrom) || ''
+ : value;
+
+ const applySetAttr = ( ) => {
+ let elems;
+ try {
+ elems = document.querySelectorAll(selector);
+ } catch {
+ return false;
+ }
+ for ( const elem of elems ) {
+ const before = elem.getAttribute(attr);
+ const after = extractValue(elem);
+ if ( after === before ) { continue; }
+ if ( after !== '' && /^on/i.test(attr) ) {
+ if ( attr.toLowerCase() in elem ) { continue; }
+ }
+ elem.setAttribute(attr, after);
+ safe.uboLog(logPrefix, `${attr}="${after}"`);
+ }
+ return true;
+ };
+
+ let observer, timer;
+ const onDomChanged = mutations => {
+ if ( timer !== undefined ) { return; }
+ let shouldWork = false;
+ for ( const mutation of mutations ) {
+ if ( mutation.addedNodes.length === 0 ) { continue; }
+ for ( const node of mutation.addedNodes ) {
+ if ( node.nodeType !== 1 ) { continue; }
+ shouldWork = true;
+ break;
+ }
+ if ( shouldWork ) { break; }
+ }
+ if ( shouldWork === false ) { return; }
+ timer = self.requestAnimationFrame(( ) => {
+ timer = undefined;
+ applySetAttr();
+ });
+ };
+
+ const start = ( ) => {
+ if ( applySetAttr() === false ) { return; }
+ observer = new MutationObserver(onDomChanged);
+ const root = document.documentElement;
+ if ( root instanceof self.Node === false ) { return; }
+ observer.observe(root, { subtree: true, childList: true });
+ };
+ runAt(( ) => { start(); }, options.runAt || 'idle');
+}
+registerScriptlet(setAttrFn, {
+ name: 'set-attr.fn',
+ dependencies: [
+ runAt,
+ safeSelf,
+ ],
+});
+
+/**
+ * @scriptlet set-attr
+ *
+ * @description
+ * Sets the specified attribute on the specified elements. This scriptlet runs
+ * once when the page loads then afterward on DOM mutations.
+ *
+ * Reference: https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-attr.js
+ *
+ * @param selector
+ * A CSS selector for the elements to target.
+ *
+ * @param attr
+ * The name of the attribute to modify.
+ *
+ * @param value
+ * The new value of the attribute. Supported values:
+ * - `''`: empty string (default)
+ * - `true`
+ * - `false`
+ * - positive decimal integer 0 <= value < 32768
+ * - `[other]`: copy the value from attribute `other` on the same element
+ *
+ * */
+
+export function setAttr(
+ selector = '',
+ attr = '',
+ value = '',
+ ...varargs
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('set-attr', selector, attr, value);
+ const validValues = [ '', 'false', 'true' ];
+ if ( validValues.includes(value.toLowerCase()) === false ) {
+ if ( /^\d+$/.test(value) ) {
+ const n = parseInt(value, 10);
+ if ( n >= 32768 ) { return; }
+ value = `${n}`;
+ } else if ( /^\[.+\]$/.test(value) === false ) {
+ return;
+ }
+ }
+ const options = safe.parseVarargs(varargs);
+ setAttrFn(false, logPrefix, selector, attr, value, options);
+}
+registerScriptlet(setAttr, {
+ name: 'set-attr.js',
+ dependencies: [
+ safeSelf,
+ setAttrFn,
+ ],
+ world: 'ISOLATED',
+});
+
+/**
+ * @trustedScriptlet trusted-set-attr
+ *
+ * @description
+ * Sets the specified attribute on the specified elements. This scriptlet runs
+ * once when the page loads then afterward on DOM mutations.
+ *
+ * Reference: https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-trusted-scriptlets.md#-%EF%B8%8F-trusted-set-attr
+ *
+ * @param selector
+ * A CSS selector for the elements to target.
+ *
+ * @param attr
+ * The name of the attribute to modify.
+ *
+ * @param value
+ * The new value of the attribute. Since the scriptlet requires a trusted
+ * source, the value can be anything.
+ *
+ * */
+
+export function trustedSetAttr(
+ selector = '',
+ attr = '',
+ value = '',
+ ...varargs
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('trusted-set-attr', selector, attr, value);
+ const options = safe.parseVarargs(varargs);
+ setAttrFn(true, logPrefix, selector, attr, value, options);
+}
+registerScriptlet(trustedSetAttr, {
+ name: 'trusted-set-attr.js',
+ requiresTrust: true,
+ dependencies: [
+ safeSelf,
+ setAttrFn,
+ ],
+ world: 'ISOLATED',
+});
+
+/**
+ * @scriptlet remove-attr
+ *
+ * @description
+ * Remove one or more attributes from a set of elements.
+ *
+ * @param attribute
+ * The name of the attribute(s) to remove. This can be a list of space-
+ * separated attribute names.
+ *
+ * @param [selector]
+ * Optional. A CSS selector for the elements to target. Default to
+ * `[attribute]`, or `[attribute1],[attribute2],...` if more than one
+ * attribute name is specified.
+ *
+ * @param [behavior]
+ * Optional. Space-separated tokens which modify the default behavior.
+ * - `stay`: Keep trying to remove the specified attribute(s) on DOM mutations.
+ *
+ * @params [...varargs]
+ * Optional, any of following pairs of parameters:
+ * - `quitAfter, sec`: where `sec` is the number of seconds after which the
+ * scriptlet ceases to be active. This has precedence over `stay` behavior.
+ * */
+
+export function removeAttr(
+ rawToken = '',
+ rawSelector = '',
+ behavior = '',
+ ...varargs
+) {
+ if ( typeof rawToken !== 'string' ) { return; }
+ if ( rawToken === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('remove-attr',
+ rawToken, rawSelector, behavior, ...varargs
+ );
+ const tokens = safe.String_split.call(rawToken, /\s*\|\s*/);
+ const selector = tokens.map(a => {
+ const b = CSS.escape(a);
+ return rawSelector.includes(`[${b}]`) ? rawSelector : `${rawSelector}[${b}]`;
+ }).join(',');
+ const lazily = /\basap\b/.test(behavior) === false;
+ const options = safe.parseVarargs(varargs);
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Target selector:\n\t${selector}`);
+ }
+ const rmattrFromNode = node => {
+ for ( const attr of tokens ) {
+ if ( node.hasAttribute(attr) === false ) { continue; }
+ node.removeAttribute(attr);
+ safe.uboLog(logPrefix, `Removed attribute '${attr}'`);
+ }
+ };
+ const rmattr = nodes => {
+ for ( const node of nodes ?? document.querySelectorAll(selector) ) {
+ rmattrFromNode(node);
+ }
+ };
+ const rmAttrLazily = ( ) => {
+ if ( rmAttrLazily.timer !== undefined ) { return; }
+ rmAttrLazily.timer = onIdleFn(( ) => {
+ rmAttrLazily.timer = undefined;
+ rmattr();
+ }, { timeout: 17 });
+ };
+ const mutationHandler = mutations => {
+ for ( const { addedNodes, removedNodes } of mutations ) {
+ for ( const node of addedNodes ) {
+ if ( node.nodeType !== 1 ) { continue; }
+ if ( lazily ) { return rmAttrLazily(); }
+ if ( node.matches(selector) ) {
+ rmattrFromNode(node);
+ }
+ if ( node.childElementCount ) {
+ rmattr(node.querySelectorAll(selector));
+ }
+ }
+ if ( lazily ) { return; }
+ for ( const node of removedNodes ) {
+ if ( node.nodeType !== 1 ) { continue; }
+ if ( node.matches(selector) ) {
+ rmattrFromNode(node);
+ }
+ }
+ }
+ };
+ const stop = ( ) => {
+ if ( start.observer ) {
+ start.observer.disconnect();
+ start.observer = undefined;
+ }
+ if ( rmAttrLazily.timer ) {
+ offIdleFn(rmAttrLazily.timer);
+ rmAttrLazily.timer = undefined;
+ }
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, 'Quitting');
+ }
+ };
+ const start = ( ) => {
+ rmattr();
+ if ( /\bstay\b/.test(behavior) === false ) {
+ if ( options.quitAfter === undefined ) { return; }
+ }
+ start.observer = new MutationObserver(mutationHandler);
+ start.observer.observe(document, {
+ attributes: true,
+ attributeFilter: tokens,
+ childList: true,
+ subtree: true,
+ });
+ if ( options.quitAfter ) {
+ runAt(( ) => {
+ self.setTimeout(stop, options.quitAfter * 1000);
+ }, 'load');
+ }
+ };
+ runAt(( ) => { start(); }, safe.String_split.call(behavior, /\s+/));
+}
+registerScriptlet(removeAttr, {
+ name: 'remove-attr.js',
+ aliases: [
+ 'ra.js',
+ ],
+ dependencies: [
+ offIdleFn,
+ onIdleFn,
+ runAt,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/base.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/base.js
new file mode 100644
index 00000000..2c54418a
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/base.js
@@ -0,0 +1,38 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+export const registeredScriptlets = [];
+
+export const registerScriptlet = (fn, details) => {
+ if ( typeof details !== 'object' ) {
+ throw new ReferenceError('Missing scriptlet details');
+ }
+ details.fn = fn;
+ fn.details = details;
+ if ( Array.isArray(details.dependencies) ) {
+ details.dependencies.forEach((fn, i, array) => {
+ if ( typeof fn !== 'function' ) { return; }
+ array[i] = fn.details.name;
+ });
+ }
+ registeredScriptlets.push(details);
+};
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/cookie.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/cookie.js
new file mode 100644
index 00000000..79cda65b
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/cookie.js
@@ -0,0 +1,446 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+export function getSafeCookieValuesFn() {
+ return [
+ 'accept', 'reject',
+ 'accepted', 'rejected', 'notaccepted',
+ 'allow', 'disallow', 'deny',
+ 'allowed', 'denied',
+ 'approved', 'disapproved',
+ 'checked', 'unchecked',
+ 'dismiss', 'dismissed',
+ 'enable', 'disable',
+ 'enabled', 'disabled',
+ 'essential', 'nonessential',
+ 'forbidden', 'forever',
+ 'hide', 'hidden',
+ 'necessary', 'required',
+ 'ok',
+ 'on', 'off',
+ 'true', 't', 'false', 'f',
+ 'yes', 'y', 'no', 'n',
+ 'all', 'none', 'functional',
+ 'granted', 'done',
+ 'decline', 'declined',
+ 'closed', 'next', 'mandatory',
+ 'disagree', 'agree',
+ 'set', 'unset',
+ 'given',
+ ];
+}
+registerScriptlet(getSafeCookieValuesFn, {
+ name: 'get-safe-cookie-values.fn',
+});
+
+/******************************************************************************/
+
+export function getAllCookiesFn() {
+ const safe = safeSelf();
+ return safe.String_split.call(document.cookie, /\s*;\s*/).map(s => {
+ const pos = s.indexOf('=');
+ if ( pos === 0 ) { return; }
+ if ( pos === -1 ) { return `${s.trim()}=`; }
+ const key = s.slice(0, pos).trim();
+ const value = s.slice(pos+1).trim();
+ return { key, value };
+ }).filter(s => s !== undefined);
+}
+registerScriptlet(getAllCookiesFn, {
+ name: 'get-all-cookies.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+export function getCookieFn(
+ name = ''
+) {
+ const safe = safeSelf();
+ for ( const s of safe.String_split.call(document.cookie, /\s*;\s*/) ) {
+ const pos = s.indexOf('=');
+ if ( pos === -1 ) { continue; }
+ if ( s.slice(0, pos) !== name ) { continue; }
+ return s.slice(pos+1).trim();
+ }
+}
+registerScriptlet(getCookieFn, {
+ name: 'get-cookie.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+export function setCookieFn(
+ trusted = false,
+ name = '',
+ value = '',
+ expires = '',
+ path = '',
+ options = {},
+) {
+ // https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/2777
+ if ( trusted === false && /[^!#$%&'*+\-.0-9A-Z[\]^_`a-z|~]/.test(name) ) {
+ name = encodeURIComponent(name);
+ }
+ // https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
+ // The characters [",] are given a pass from the RFC requirements because
+ // apparently browsers do not follow the RFC to the letter.
+ if ( /[^ -:<-[\]-~]/.test(value) ) {
+ value = encodeURIComponent(value);
+ }
+
+ const cookieBefore = getCookieFn(name);
+ if ( cookieBefore !== undefined && options.dontOverwrite ) { return; }
+ if ( cookieBefore === value && options.reload ) { return; }
+
+ const cookieParts = [ name, '=', value ];
+ if ( expires !== '' ) {
+ cookieParts.push('; expires=', expires);
+ }
+
+ if ( path === '' ) { path = '/'; }
+ else if ( path === 'none' ) { path = ''; }
+ if ( path !== '' && path !== '/' ) { return; }
+ if ( path === '/' ) {
+ cookieParts.push('; path=/');
+ }
+
+ if ( trusted ) {
+ if ( options.domain ) {
+ let domain = options.domain;
+ if ( /^\/.+\//.test(domain) ) {
+ const baseURL = new URL(document.baseURI);
+ const reDomain = new RegExp(domain.slice(1, -1));
+ const match = reDomain.exec(baseURL.hostname);
+ domain = match ? match[0] : undefined;
+ }
+ if ( domain ) {
+ cookieParts.push(`; domain=${domain}`);
+ }
+ }
+ cookieParts.push('; Secure');
+ } else if ( /^__(Host|Secure)-/.test(name) ) {
+ cookieParts.push('; Secure');
+ }
+
+ try {
+ document.cookie = cookieParts.join('');
+ } catch {
+ }
+
+ const done = getCookieFn(name) === value;
+ if ( done && options.reload ) {
+ window.location.reload();
+ }
+
+ return done;
+}
+registerScriptlet(setCookieFn, {
+ name: 'set-cookie.fn',
+ dependencies: [
+ getCookieFn,
+ ],
+});
+
+/**
+ * @scriptlet set-cookie
+ *
+ * @description
+ * Set a cookie to a safe value.
+ *
+ * @param name
+ * The name of the cookie to set.
+ *
+ * @param value
+ * The value of the cookie to set. Must be a safe value. Unsafe values will be
+ * ignored and no cookie will be set. See getSafeCookieValuesFn() helper above.
+ *
+ * @param [path]
+ * Optional. The path of the cookie to set. Default to `/`.
+ *
+ * Reference:
+ * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js
+ * */
+
+export function setCookie(
+ name = '',
+ value = '',
+ path = '',
+ ...varargs
+) {
+ if ( name === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path);
+ const normalized = value.toLowerCase();
+ const match = /^("?)(.+)\1$/.exec(normalized);
+ const unquoted = match && match[2] || normalized;
+ const validValues = getSafeCookieValuesFn();
+ if ( validValues.includes(unquoted) === false ) {
+ if ( /^-?\d+$/.test(unquoted) === false ) { return; }
+ const n = parseInt(value, 10) || 0;
+ if ( n < -32767 || n > 32767 ) { return; }
+ }
+
+ const done = setCookieFn(
+ false,
+ name,
+ value,
+ '',
+ path,
+ safe.parseVarargs(varargs)
+ );
+
+ if ( done ) {
+ safe.uboLog(logPrefix, 'Done');
+ }
+}
+registerScriptlet(setCookie, {
+ name: 'set-cookie.js',
+ world: 'ISOLATED',
+ dependencies: [
+ getSafeCookieValuesFn,
+ safeSelf,
+ setCookieFn,
+ ],
+});
+
+// For compatibility with AdGuard
+export function setCookieReload(name, value, path, ...args) {
+ setCookie(name, value, path, 'reload', '1', ...args);
+}
+registerScriptlet(setCookieReload, {
+ name: 'set-cookie-reload.js',
+ world: 'ISOLATED',
+ dependencies: [
+ setCookie,
+ ],
+});
+
+/**
+ * @trustedScriptlet trusted-set-cookie
+ *
+ * @description
+ * Set a cookie to any value. This scriptlet can be used only from a trusted
+ * source.
+ *
+ * @param name
+ * The name of the cookie to set.
+ *
+ * @param value
+ * The value of the cookie to set. Must be a safe value. Unsafe values will be
+ * ignored and no cookie will be set. See getSafeCookieValuesFn() helper above.
+ *
+ * @param [offsetExpiresSec]
+ * Optional. The path of the cookie to set. Default to `/`.
+ *
+ * @param [path]
+ * Optional. The path of the cookie to set. Default to `/`.
+ *
+ * Reference:
+ * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js
+ * */
+
+export function trustedSetCookie(
+ name = '',
+ value = '',
+ offsetExpiresSec = '',
+ path = '',
+ ...varargs
+) {
+ if ( name === '' ) { return; }
+
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path);
+ const time = new Date();
+
+ if ( value.includes('$now$') ) {
+ value = value.replaceAll('$now$', time.getTime());
+ }
+ if ( value.includes('$currentDate$') ) {
+ value = value.replaceAll('$currentDate$', time.toUTCString());
+ }
+ if ( value.includes('$currentISODate$') ) {
+ value = value.replaceAll('$currentISODate$', time.toISOString());
+ }
+
+ let expires = '';
+ if ( offsetExpiresSec !== '' ) {
+ if ( offsetExpiresSec === '1day' ) {
+ time.setDate(time.getDate() + 1);
+ } else if ( offsetExpiresSec === '1year' ) {
+ time.setFullYear(time.getFullYear() + 1);
+ } else {
+ if ( /^\d+$/.test(offsetExpiresSec) === false ) { return; }
+ time.setSeconds(time.getSeconds() + parseInt(offsetExpiresSec, 10));
+ }
+ expires = time.toUTCString();
+ }
+
+ const done = setCookieFn(
+ true,
+ name,
+ value,
+ expires,
+ path,
+ safe.parseVarargs(varargs)
+ );
+
+ if ( done ) {
+ safe.uboLog(logPrefix, 'Done');
+ }
+}
+registerScriptlet(trustedSetCookie, {
+ name: 'trusted-set-cookie.js',
+ requiresTrust: true,
+ world: 'ISOLATED',
+ dependencies: [
+ safeSelf,
+ setCookieFn,
+ ],
+});
+
+// For compatibility with AdGuard
+export function trustedSetCookieReload(name, value, offsetExpiresSec, path, ...args) {
+ trustedSetCookie(name, value, offsetExpiresSec, path, 'reload', '1', ...args);
+}
+registerScriptlet(trustedSetCookieReload, {
+ name: 'trusted-set-cookie-reload.js',
+ requiresTrust: true,
+ world: 'ISOLATED',
+ dependencies: [
+ trustedSetCookie,
+ ],
+});
+
+/**
+ * @scriptlet remove-cookie
+ *
+ * @description
+ * Removes current site cookies specified by name. The removal operation occurs
+ * immediately when the scriptlet is injected, then when the page is unloaded.
+ *
+ * @param needle
+ * A string or a regex matching the name of the cookie(s) to remove.
+ *
+ * @param ['when', token]
+ * Vararg, optional. The parameter following 'when' tells when extra removal
+ * operations should take place.
+ * - `scroll`: when the page is scrolled
+ * - `keydown`: when a keyboard touch is pressed
+ *
+ * */
+
+export function removeCookie(
+ needle = '',
+ ...varargs
+) {
+ if ( typeof needle !== 'string' ) { return; }
+ const safe = safeSelf();
+ const reName = safe.patternToRegex(needle);
+ const extraArgs = safe.parseVarargs(varargs);
+ const throttle = (fn, ms = 500) => {
+ if ( throttle.timer !== undefined ) { return; }
+ throttle.timer = setTimeout(( ) => {
+ throttle.timer = undefined;
+ fn();
+ }, ms);
+ };
+ const baseURL = new URL(document.baseURI);
+ let targetDomain = extraArgs.domain;
+ if ( targetDomain && /^\/.+\//.test(targetDomain) ) {
+ const reDomain = new RegExp(targetDomain.slice(1, -1));
+ const match = reDomain.exec(baseURL.hostname);
+ targetDomain = match ? match[0] : undefined;
+ }
+ const remove = ( ) => {
+ safe.String_split.call(document.cookie, ';').forEach(cookieStr => {
+ const pos = cookieStr.indexOf('=');
+ if ( pos === -1 ) { return; }
+ const cookieName = cookieStr.slice(0, pos).trim();
+ if ( reName.test(cookieName) === false ) { return; }
+ const part1 = cookieName + '=';
+ const part2a = `; domain=${baseURL.hostname}`;
+ const part2b = `; domain=.${baseURL.hostname}`;
+ let part2c, part2d;
+ if ( targetDomain ) {
+ part2c = `; domain=${targetDomain}`;
+ part2d = `; domain=.${targetDomain}`;
+ } else if ( document.domain ) {
+ const domain = document.domain;
+ if ( domain !== baseURL.hostname ) {
+ part2c = `; domain=.${domain}`;
+ }
+ if ( domain.startsWith('www.') ) {
+ part2d = `; domain=${domain.replace('www', '')}`;
+ }
+ }
+ const part3 = '; path=/';
+ const part4 = '; Max-Age=-1000; expires=Thu, 01 Jan 1970 00:00:00 GMT';
+ document.cookie = part1 + part4;
+ document.cookie = part1 + part2a + part4;
+ document.cookie = part1 + part2b + part4;
+ document.cookie = part1 + part3 + part4;
+ document.cookie = part1 + part2a + part3 + part4;
+ document.cookie = part1 + part2b + part3 + part4;
+ if ( part2c !== undefined ) {
+ document.cookie = part1 + part2c + part3 + part4;
+ }
+ if ( part2d !== undefined ) {
+ document.cookie = part1 + part2d + part3 + part4;
+ }
+ });
+ };
+ remove();
+ window.addEventListener('beforeunload', remove);
+ if ( typeof extraArgs.when !== 'string' ) { return; }
+ const supportedEventTypes = [ 'scroll', 'keydown' ];
+ const eventTypes = safe.String_split.call(extraArgs.when, /\s/);
+ for ( const type of eventTypes ) {
+ if ( supportedEventTypes.includes(type) === false ) { continue; }
+ document.addEventListener(type, ( ) => {
+ throttle(remove);
+ }, { passive: true });
+ }
+}
+registerScriptlet(removeCookie, {
+ name: 'remove-cookie.js',
+ aliases: [
+ 'cookie-remover.js',
+ ],
+ world: 'ISOLATED',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/create-html.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/create-html.js
new file mode 100644
index 00000000..468083d3
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/create-html.js
@@ -0,0 +1,144 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2025-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { runAt } from './run-at.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+/**
+ * @scriptlet trusted-create-html
+ *
+ * @description
+ * Element(s) from a parsed HTML string are added as child element(s) to a
+ * specific parent element in the DOM.
+ *
+ * @param parent
+ * A CSS selector identifying the element to which created element(s) will be
+ * added.
+ *
+ * @param html
+ * An HTML string to be parsed using DOMParser, and which resulting elements
+ * are to be added as child element(s).
+ *
+ * @param duration
+ * Optional. If specified, the time in ms after which the added elements will
+ * be removed. No removal will occur if not specified.
+ *
+ * */
+
+function trustedCreateHTML(
+ parentSelector,
+ htmlStr = '',
+ durationStr = '',
+ ...varargs
+) {
+ if ( parentSelector === '' ) { return; }
+ if ( htmlStr === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('trusted-create-html', parentSelector, htmlStr, durationStr);
+ const extraArgs = safe.parseVarargs(varargs);
+ // We do not want to recursively create elements
+ self.trustedCreateHTML = true;
+ let ancestor = self.frameElement;
+ while ( ancestor ) {
+ const doc = ancestor.ownerDocument;
+ if ( doc === null ) { break; }
+ const win = doc.defaultView;
+ if ( win === null ) { break; }
+ if ( win.trustedCreateHTML ) { return; }
+ ancestor = ancestor.frameElement;
+ }
+ const duration = parseInt(durationStr, 10);
+ const domParser = new DOMParser();
+ const externalDoc = domParser.parseFromString(htmlStr, 'text/html');
+ const toAppend = [];
+ while ( externalDoc.body.firstChild !== null ) {
+ toAppend.push(document.adoptNode(externalDoc.body.firstChild));
+ }
+ if ( toAppend.length === 0 ) { return; }
+ const toRemove = [];
+ const remove = ( ) => {
+ for ( const node of toRemove ) {
+ if ( node.parentNode === null ) { continue; }
+ node.parentNode.removeChild(node);
+ }
+ safe.uboLog(logPrefix, 'Node(s) removed');
+ };
+ const appendOne = (target, nodes) => {
+ for ( const node of nodes ) {
+ target.append(node);
+ if ( isNaN(duration) ) { continue; }
+ toRemove.push(node);
+ }
+ };
+ const append = ( ) => {
+ const targets = document.querySelectorAll(parentSelector);
+ if ( targets.length === 0 ) { return false; }
+ const limit = Math.min(targets.length, extraArgs.limit || 1) - 1;
+ for ( let i = 0; i < limit; i++ ) {
+ appendOne(targets[i], toAppend.map(a => a.cloneNode(true)));
+ }
+ appendOne(targets[limit], toAppend);
+ safe.uboLog(logPrefix, 'Node(s) appended');
+ if ( toRemove.length === 0 ) { return true; }
+ setTimeout(remove, duration);
+ return true;
+ };
+ const start = ( ) => {
+ if ( append() ) { return; }
+ const observer = new MutationObserver(( ) => {
+ if ( append() === false ) { return; }
+ observer.disconnect();
+ });
+ const observerOptions = {
+ childList: true,
+ subtree: true,
+ };
+ if ( /[#.[]/.test(parentSelector) ) {
+ observerOptions.attributes = true;
+ if ( parentSelector.includes('[') === false ) {
+ observerOptions.attributeFilter = [];
+ if ( parentSelector.includes('#') ) {
+ observerOptions.attributeFilter.push('id');
+ }
+ if ( parentSelector.includes('.') ) {
+ observerOptions.attributeFilter.push('class');
+ }
+ }
+ }
+ observer.observe(document, observerOptions);
+ };
+ runAt(start, extraArgs.runAt || 'loading');
+}
+registerScriptlet(trustedCreateHTML, {
+ name: 'trusted-create-html.js',
+ requiresTrust: true,
+ dependencies: [
+ runAt,
+ safeSelf,
+ ],
+ world: 'ISOLATED',
+});
+
+/******************************************************************************/
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/href-sanitizer.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/href-sanitizer.js
new file mode 100644
index 00000000..3be7066f
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/href-sanitizer.js
@@ -0,0 +1,172 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { onIdleFn } from './utils.js';
+import { registerScriptlet } from './base.js';
+import { runAt } from './run-at.js';
+import { safeSelf } from './safe-self.js';
+import { urlSkip } from '../urlskip.js';
+
+/******************************************************************************/
+
+registerScriptlet(urlSkip, {
+ name: 'urlskip.fn',
+});
+
+/**
+ * @scriptlet href-sanitizer
+ *
+ * @description
+ * Set the `href` attribute to a value found in the DOM at, or below the
+ * targeted `a` element, and optionally with transformation steps.
+ *
+ * @param selector
+ * A plain CSS selector for elements which `href` property must be sanitized.
+ *
+ * @param source
+ * One or more tokens to lookup the source of the `href` property, and
+ * optionally the transformation steps to perform:
+ * - `text`: Use the text content of the element as the URL
+ * - `[name]`: Use the value of the attribute `name` as the URL
+ * - Transformation steps: see `urlskip` documentation
+ *
+ * If `text` or `[name]` is not present, the URL will be the value of `href`
+ * attribute.
+ *
+ * @example
+ * `example.org##+js(href-sanitizer, a)`
+ * `example.org##+js(href-sanitizer, a[title], [title])`
+ * `example.org##+js(href-sanitizer, a[href*="/away.php?to="], ?to)`
+ * `example.org##+js(href-sanitizer, a[href*="/redirect"], ?url ?url -base64)`
+ *
+ * */
+
+function hrefSanitizer(
+ selector = '',
+ source = ''
+) {
+ if ( typeof selector !== 'string' ) { return; }
+ if ( selector === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('href-sanitizer', selector, source);
+ if ( source === '' ) { source = 'text'; }
+ const sanitizeCopycats = (href, text) => {
+ let elems = [];
+ try {
+ elems = document.querySelectorAll(`a[href="${href}"`);
+ }
+ catch {
+ }
+ for ( const elem of elems ) {
+ elem.setAttribute('href', text);
+ }
+ return elems.length;
+ };
+ const validateURL = text => {
+ if ( typeof text !== 'string' ) { return ''; }
+ if ( text === '' ) { return ''; }
+ if ( /[\x00-\x20\x7f]/.test(text) ) { return ''; }
+ try {
+ const url = new URL(text, document.location);
+ return url.href;
+ } catch {
+ }
+ return '';
+ };
+ const extractURL = (elem, source) => {
+ if ( /^\[.*\]$/.test(source) ) {
+ return elem.getAttribute(source.slice(1,-1).trim()) || '';
+ }
+ if ( source === 'text' ) {
+ return elem.textContent
+ .replace(/^[^\x21-\x7e]+/, '') // remove leading invalid characters
+ .replace(/[^\x21-\x7e]+$/, ''); // remove trailing invalid characters
+ }
+ const steps = source.replace(/(\S)\?/g, '\\1 ?').split(/\s+/);
+ const url = urlSkip(elem.href, false, steps);
+ if ( url === undefined ) { return; }
+ return url.replace(/ /g, '%20');
+ };
+ const sanitize = ( ) => {
+ let elems = [];
+ try {
+ elems = document.querySelectorAll(selector);
+ }
+ catch {
+ return false;
+ }
+ for ( const elem of elems ) {
+ if ( elem.localName !== 'a' ) { continue; }
+ if ( elem.hasAttribute('href') === false ) { continue; }
+ const href = elem.getAttribute('href');
+ const text = extractURL(elem, source);
+ const hrefAfter = validateURL(text);
+ if ( hrefAfter === '' ) { continue; }
+ if ( hrefAfter === href ) { continue; }
+ elem.setAttribute('href', hrefAfter);
+ const count = sanitizeCopycats(href, hrefAfter);
+ safe.uboLog(logPrefix, `Sanitized ${count+1} links to\n${hrefAfter}`);
+ }
+ return true;
+ };
+ let observer, timer;
+ const onDomChanged = mutations => {
+ if ( timer !== undefined ) { return; }
+ let shouldSanitize = false;
+ for ( const mutation of mutations ) {
+ if ( mutation.addedNodes.length === 0 ) { continue; }
+ for ( const node of mutation.addedNodes ) {
+ if ( node.nodeType !== 1 ) { continue; }
+ shouldSanitize = true;
+ break;
+ }
+ if ( shouldSanitize ) { break; }
+ }
+ if ( shouldSanitize === false ) { return; }
+ timer = onIdleFn(( ) => {
+ timer = undefined;
+ sanitize();
+ });
+ };
+ const start = ( ) => {
+ if ( sanitize() === false ) { return; }
+ observer = new MutationObserver(onDomChanged);
+ observer.observe(document.body, {
+ subtree: true,
+ childList: true,
+ });
+ };
+ runAt(( ) => { start(); }, 'interactive');
+}
+registerScriptlet(hrefSanitizer, {
+ name: 'href-sanitizer.js',
+ world: 'ISOLATED',
+ aliases: [
+ 'urlskip.js',
+ ],
+ dependencies: [
+ onIdleFn,
+ runAt,
+ safeSelf,
+ urlSkip,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/json-edit.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/json-edit.js
new file mode 100644
index 00000000..66727250
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/json-edit.js
@@ -0,0 +1,1672 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import {
+ collateFetchArgumentsFn,
+ lookupElementsFn,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ sleepFn,
+ trapPropertyFn,
+} from './utils.js';
+
+import { JSONPath } from './shared.js';
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+/******************************************************************************/
+
+function editOutboundObjectFn(
+ trusted = false,
+ propChain = '',
+ jsonq = ''
+) {
+ if ( propChain === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}edit-outbound-object`,
+ propChain,
+ jsonq
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ proxyApplyFn(propChain, function(context) {
+ const obj = context.reflect();
+ const objAfter = jsonp.apply(obj);
+ if ( objAfter === undefined ) { return obj; }
+ safe.uboLog(logPrefix, 'Edited');
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
+ }
+ return objAfter;
+ });
+}
+registerScriptlet(editOutboundObjectFn, {
+ name: 'edit-outbound-object.fn',
+ dependencies: [
+ JSONPath,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet edit-outbound-object-.js
+ *
+ * @description
+ * Prune properties from an object returned by a specific method.
+ * Properties can only be removed.
+ *
+ * @param propChain
+ * Property chain of the method to trap.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * */
+
+function editOutboundObject(propChain = '', jsonq = '') {
+ editOutboundObjectFn(false, propChain, jsonq);
+}
+registerScriptlet(editOutboundObject, {
+ name: 'edit-outbound-object.js',
+ dependencies: [
+ editOutboundObjectFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-edit-outbound-object.js
+ *
+ * @description
+ * Edit properties of an object returned by a specific method.
+ * Properties can be assigned new values.
+ *
+ * @param propChain
+ * Property chain of the method to trap.
+ *
+* @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * */
+
+function trustedEditOutboundObject(propChain = '', jsonq = '') {
+ editOutboundObjectFn(true, propChain, jsonq);
+}
+registerScriptlet(trustedEditOutboundObject, {
+ name: 'trusted-edit-outbound-object.js',
+ requiresTrust: true,
+ dependencies: [
+ editOutboundObjectFn,
+ ],
+});
+
+/******************************************************************************/
+
+function jsonEditFn(trusted = false, jsonq = '', ...varargs) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}json-edit`,
+ jsonq,
+ ...varargs
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const extraArgs = safe.parseVarargs(varargs);
+ const pattern = extraArgs.matches && safe.initPattern(extraArgs.matches);
+ proxyApplyFn('JSON.parse', function(context) {
+ const json = context.callArgs[0];
+ const obj = context.reflect();
+ if ( pattern && typeof json === 'string' ) {
+ if ( safe.testPattern(pattern, json) === false ) { return obj; }
+ }
+ const objAfter = jsonp.apply(obj);
+ if ( objAfter === undefined ) { return obj; }
+ safe.uboLog(logPrefix, 'Edited');
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
+ }
+ return objAfter;
+ });
+}
+registerScriptlet(jsonEditFn, {
+ name: 'json-edit.fn',
+ dependencies: [
+ JSONPath,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet json-edit.js
+ *
+ * @description
+ * Edit object generated through JSON.parse().
+ * Properties can only be removed.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param 'matches', pattern
+ * Vararg, optional: The JSONPath will be applied if and only if the pattern
+ * matches the inbound JSON string. The pattern can be a plain string or a
+ * regex.
+ *
+ * */
+
+function jsonEdit(jsonq = '', ...varargs) {
+ jsonEditFn(false, jsonq, ...varargs);
+}
+registerScriptlet(jsonEdit, {
+ name: 'json-edit.js',
+ dependencies: [
+ jsonEditFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-json-edit.js
+ *
+ * @description
+ * Edit object generated through JSON.parse().
+ * Properties can be assigned new values.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param 'matches', pattern
+ * Vararg, optional: The JSONPath will be applied if and only if the pattern
+ * matches the inbound JSON string. The pattern can be a plain string or a
+ * regex.
+ *
+ * */
+
+function trustedJsonEdit(jsonq = '', ...varargs) {
+ jsonEditFn(true, jsonq, ...varargs);
+}
+registerScriptlet(trustedJsonEdit, {
+ name: 'trusted-json-edit.js',
+ requiresTrust: true,
+ dependencies: [
+ jsonEditFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function editInboundObjectFn(
+ trusted = false,
+ propChain = '',
+ argPosRaw = '',
+ jsonq = '',
+) {
+ if ( propChain === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}edit-inbound-object`,
+ propChain,
+ jsonq
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const argPos = parseInt(argPosRaw, 10);
+ if ( isNaN(argPos) ) { return; }
+ const getArgPos = args => {
+ if ( Array.isArray(args) === false ) { return; }
+ if ( argPos >= 0 ) {
+ if ( args.length <= argPos ) { return; }
+ return argPos;
+ }
+ if ( args.length < -argPos ) { return; }
+ return args.length + argPos;
+ };
+ const editObj = obj => {
+ let clone;
+ try {
+ clone = safe.structuredClone(obj);
+ } catch {
+ }
+ if ( typeof clone !== 'object' || clone === null ) { return; }
+ const objAfter = jsonp.apply(clone);
+ if ( objAfter === undefined ) { return; }
+ safe.uboLog(logPrefix, 'Edited');
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
+ }
+ return objAfter;
+ };
+ proxyApplyFn(propChain, function(context) {
+ const i = getArgPos(context.callArgs);
+ if ( i !== undefined ) {
+ const objBefore = context.callArgs[i];
+ if ( typeof objBefore === 'object' ) {
+ const objAfter = editObj(objBefore);
+ if ( objAfter ) {
+ context.callArgs[i] = objAfter;
+ }
+ }
+ }
+ return context.reflect();
+ });
+}
+registerScriptlet(editInboundObjectFn, {
+ name: 'edit-inbound-object.fn',
+ dependencies: [
+ JSONPath,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet edit-inbound-object.js
+ *
+ * @description
+ * Prune properties from an object passed as argument to a specific method.
+ * Properties can only be removed.
+ *
+ * @param propChain
+ * Property chain of the method to trap.
+ *
+ * @param argPos
+ * 0-based position of the argument. Use negative integer for position relative
+ * to the end.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * */
+
+function editInboundObject(propChain = '', argPos = '', jsonq = '') {
+ editInboundObjectFn(false, propChain, argPos, jsonq);
+}
+registerScriptlet(editInboundObject, {
+ name: 'edit-inbound-object.js',
+ dependencies: [
+ editInboundObjectFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-edit-inbound-object.js
+ *
+ * @description
+ * Edit properties of an object passed as argument to a specific method.
+ * Properties can be assigned new values.
+ *
+ * @param propChain
+ * Property chain of the method to trap.
+ *
+ * @param argPos
+ * 0-based position of the argument. Use negative integer for position relative
+ * to the end.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * */
+
+function trustedEditInboundObject(propChain = '', argPos = '', jsonq = '') {
+ editInboundObjectFn(true, propChain, argPos, jsonq);
+}
+registerScriptlet(trustedEditInboundObject, {
+ name: 'trusted-edit-inbound-object.js',
+ requiresTrust: true,
+ dependencies: [
+ editInboundObjectFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function editThisObjectFn(
+ trusted = false,
+ propChain = '',
+ jsonq = '',
+ order = ''
+) {
+ if ( propChain === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}edit-this-object`,
+ propChain,
+ jsonq
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const editObj = objBefore => {
+ const objAfter = jsonp.apply(objBefore);
+ if ( objAfter === undefined ) { return; }
+ safe.uboLog(logPrefix, 'Edited');
+ if ( safe.logLevel <= 1 ) { return; }
+ safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
+ };
+ proxyApplyFn(propChain, function(context) {
+ const { thisArg } = context;
+ let r;
+ if ( order === 'after' ) {
+ r = context.reflect();
+ }
+ editObj(thisArg);
+ if ( order !== 'after' ) {
+ r = context.reflect();
+ }
+ return r;
+ });
+}
+registerScriptlet(editThisObjectFn, {
+ name: 'edit-this-object.fn',
+ dependencies: [
+ JSONPath,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet edit-this-object.js
+ *
+ * @description
+ * Prune properties from an object through one of the object's own method.
+ * Properties can only be removed.
+ *
+ * @param propChain
+ * Property chain of the method to trap.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param order
+ * If set to `after`, the `this` object will be edited after the trapped method
+ * is called. By default the `this` object is edited before calling the trapped
+ * method.
+ *
+ * */
+
+function editThisObject(...args) {
+ editThisObjectFn(false, ...args);
+}
+registerScriptlet(editThisObject, {
+ name: 'edit-this-object.js',
+ dependencies: [
+ editThisObjectFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-edit-this-object.js
+ *
+ * @description
+ * Edit properties of an object through one of the object's own method.
+ * Properties can be assigned new values.
+ *
+ * @param propChain
+ * Property chain of the method to trap.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param order
+ * If set to `after`, the `this` object will be edited after the trapped method
+ * is called. By default the `this` object is edited before calling the trapped
+ * method.
+ *
+ * */
+
+function trustedEditThisObject(...args) {
+ editThisObjectFn(true, ...args);
+}
+registerScriptlet(trustedEditThisObject, {
+ name: 'trusted-edit-this-object.js',
+ requiresTrust: true,
+ dependencies: [
+ editThisObjectFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function editObjectOnGetterFn(
+ trusted = false,
+ propChain = '',
+ jsonq = '',
+) {
+ if ( propChain === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}edit-object-on-getter`,
+ propChain,
+ jsonq
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const editObj = objBefore => {
+ const objAfter = jsonp.apply(objBefore);
+ if ( objAfter === undefined ) { return; }
+ safe.uboLog(logPrefix, 'Edited');
+ if ( safe.logLevel <= 1 ) { return; }
+ safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
+ };
+ let currentValue = trapPropertyFn(propChain, {
+ get: function() {
+ if ( currentValue instanceof Object ) {
+ editObj(currentValue);
+ }
+ return currentValue;
+ },
+ set: function(a) {
+ currentValue = a;
+ }
+ });
+}
+registerScriptlet(editObjectOnGetterFn, {
+ name: 'edit-object-on-getter.fn',
+ dependencies: [
+ JSONPath,
+ safeSelf,
+ trapPropertyFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet edit-object-on-getter.js
+ *
+ * @description
+ * Prune properties from an object on read access.
+ * Properties can only be removed.
+ *
+ * @param propChain
+ * Property chain of the property to trap.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * */
+
+function editObjectOnGetter(...args) {
+ editObjectOnGetterFn(false, ...args);
+}
+registerScriptlet(editObjectOnGetter, {
+ name: 'edit-object-on-getter.js',
+ dependencies: [
+ editObjectOnGetterFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-edit-object-on-getter.js
+ *
+ * @description
+ * Edit properties of an object on read access.
+ * Properties can be assigned new values.
+ *
+ * @param propChain
+ * Property chain of the property to trap.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * */
+
+function trustedEditObjectOnGetter(...args) {
+ editObjectOnGetterFn(true, ...args);
+}
+registerScriptlet(trustedEditObjectOnGetter, {
+ name: 'trusted-edit-object-on-getter.js',
+ requiresTrust: true,
+ dependencies: [
+ editObjectOnGetterFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function editObjectOnSetterFn(
+ trusted = false,
+ propChain = '',
+ jsonq = '',
+) {
+ if ( propChain === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}edit-object-on-setter`,
+ propChain,
+ jsonq
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const editObj = objBefore => {
+ const objAfter = jsonp.apply(objBefore);
+ if ( objAfter === undefined ) { return; }
+ safe.uboLog(logPrefix, 'Edited');
+ if ( safe.logLevel <= 1 ) { return; }
+ safe.uboLog(logPrefix, `After edit:\n${safe.JSON_stringify(objAfter, null, 2)}`);
+ };
+ let currentValue = trapPropertyFn(propChain, {
+ get: function() {
+ return currentValue;
+ },
+ set: function(a) {
+ currentValue = a;
+ if ( currentValue instanceof Object ) {
+ editObj(currentValue);
+ }
+ }
+ });
+ if ( currentValue instanceof Object ) {
+ editObj(currentValue);
+ }
+}
+registerScriptlet(editObjectOnSetterFn, {
+ name: 'edit-object-on-setter.fn',
+ dependencies: [
+ JSONPath,
+ safeSelf,
+ trapPropertyFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet edit-object-on-setter.js
+ *
+ * @description
+ * Prune properties from an object on write access.
+ * Properties can only be removed.
+ *
+ * @param propChain
+ * Property chain of the property to trap.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * */
+
+function editObjectOnSetter(...args) {
+ editObjectOnSetterFn(false, ...args);
+}
+registerScriptlet(editObjectOnSetter, {
+ name: 'edit-object-on-setter.js',
+ dependencies: [
+ editObjectOnSetterFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-edit-object-on-setter.js
+ *
+ * @description
+ * Edit properties of an object on write access.
+ * Properties can be assigned new values.
+ *
+ * @param propChain
+ * Property chain of the property to trap.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * */
+
+function trustedEditObjectOnSetter(...args) {
+ editObjectOnSetterFn(true, ...args);
+}
+registerScriptlet(trustedEditObjectOnSetter, {
+ name: 'trusted-edit-object-on-setter.js',
+ requiresTrust: true,
+ dependencies: [
+ editObjectOnSetterFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+async function editElementObjectFn(
+ trusted = false,
+ selector = '',
+ jsonq = '',
+ timeout = ''
+) {
+ if ( selector === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}edit-element-object`,
+ selector, jsonq, timeout
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const process = elems => {
+ for ( const elem of elems ) {
+ const r = jsonp.apply(elem);
+ if ( r === undefined ) { continue; }
+ safe.uboLog(logPrefix, 'Edited');
+ }
+ };
+ const until = timeout !== '' ? Date.now() + parseInt(timeout, 10) : 0;
+ do {
+ const r = lookupElementsFn(selector, until);
+ if ( Array.isArray(r) ) {
+ process(r);
+ } else if ( r instanceof Promise ) {
+ const elems = await r;
+ process(elems);
+ }
+ await sleepFn();
+ } while ( Date.now() < until );
+}
+registerScriptlet(editElementObjectFn, {
+ name: 'edit-element-object.fn',
+ dependencies: [
+ JSONPath,
+ lookupElementsFn,
+ safeSelf,
+ sleepFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet edit-element-object.js
+ *
+ * @description
+ * Prune properties of one or more elements matching a specific selector.
+ * Properties can only be removed.
+ *
+ * @param selector
+ * The selector used to locate the elements on which the JSONPath query will
+ * be applied.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param timeout
+ * The maximum time (in milliseconds) to wait for matching elements before
+ * stopping. Defaults to 0, which executes a single lookup attempt without
+ * waiting.
+ *
+ * */
+
+function editElementObject(...args) {
+ editElementObjectFn(false, ...args);
+}
+registerScriptlet(editElementObject, {
+ name: 'edit-element-object.js',
+ dependencies: [
+ editElementObjectFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-edit-element-object.js
+ *
+ * @description
+ * Edit properties of one or more elements matching a specific selector.
+ * Properties can be assigned new values.
+ *
+ * @param selector
+ * The selector used to locate the elements on which the JSONPath query will
+ * be applied.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param timeout
+ * The maximum time (in milliseconds) to wait for matching elements before
+ * stopping. Defaults to 0, which executes a single lookup attempt without
+ * waiting.
+ *
+ * */
+
+function trustedEditElementObject(...args) {
+ editElementObjectFn(true, ...args);
+}
+registerScriptlet(trustedEditElementObject, {
+ name: 'trusted-edit-element-object.js',
+ requiresTrust: true,
+ dependencies: [
+ editElementObjectFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function jsonEditXhrResponseFn(trusted, jsonq = '', ...varargs) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}json-edit-xhr-response`,
+ jsonq
+ );
+ const xhrInstances = new WeakMap();
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const extraArgs = safe.parseVarargs(varargs);
+ const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
+ self.XMLHttpRequest = class extends self.XMLHttpRequest {
+ open(method, url, ...args) {
+ const xhrDetails = { method, url };
+ const matched = propNeedles.size === 0 ||
+ matchObjectPropertiesFn(propNeedles, xhrDetails);
+ if ( matched ) {
+ if ( safe.logLevel > 1 && Array.isArray(matched) ) {
+ safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`);
+ }
+ xhrInstances.set(this, xhrDetails);
+ }
+ return super.open(method, url, ...args);
+ }
+ get response() {
+ const innerResponse = super.response;
+ const xhrDetails = xhrInstances.get(this);
+ if ( xhrDetails === undefined ) { return innerResponse; }
+ const responseLength = typeof innerResponse === 'string'
+ ? innerResponse.length
+ : undefined;
+ if ( xhrDetails.lastResponseLength !== responseLength ) {
+ xhrDetails.response = undefined;
+ xhrDetails.lastResponseLength = responseLength;
+ }
+ if ( xhrDetails.response !== undefined ) {
+ return xhrDetails.response;
+ }
+ let obj;
+ if ( typeof innerResponse === 'object' ) {
+ obj = innerResponse;
+ } else if ( typeof innerResponse === 'string' ) {
+ try { obj = safe.JSON_parse(innerResponse); } catch { }
+ }
+ if ( typeof obj !== 'object' || obj === null ) {
+ return (xhrDetails.response = innerResponse);
+ }
+ const objAfter = jsonp.apply(obj);
+ if ( objAfter === undefined ) {
+ return (xhrDetails.response = innerResponse);
+ }
+ safe.uboLog(logPrefix, 'Edited');
+ const outerResponse = typeof innerResponse === 'string'
+ ? JSONPath.toJSON(objAfter, safe.JSON_stringify)
+ : objAfter;
+ return (xhrDetails.response = outerResponse);
+ }
+ get responseText() {
+ const response = this.response;
+ return typeof response !== 'string'
+ ? super.responseText
+ : response;
+ }
+ };
+}
+registerScriptlet(jsonEditXhrResponseFn, {
+ name: 'json-edit-xhr-response.fn',
+ dependencies: [
+ JSONPath,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet json-edit-xhr-response.js
+ *
+ * @description
+ * Edit the object fetched through a XHR instance.
+ * Properties can only be removed.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function jsonEditXhrResponse(jsonq = '', ...args) {
+ jsonEditXhrResponseFn(false, jsonq, ...args);
+}
+registerScriptlet(jsonEditXhrResponse, {
+ name: 'json-edit-xhr-response.js',
+ dependencies: [
+ jsonEditXhrResponseFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-json-edit-xhr-response.js
+ *
+ * @description
+ * Edit the object fetched through a XHR instance.
+ * Properties can be assigned new values.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function trustedJsonEditXhrResponse(jsonq = '', ...args) {
+ jsonEditXhrResponseFn(true, jsonq, ...args);
+}
+registerScriptlet(trustedJsonEditXhrResponse, {
+ name: 'trusted-json-edit-xhr-response.js',
+ requiresTrust: true,
+ dependencies: [
+ jsonEditXhrResponseFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function jsonEditXhrRequestFn(trusted, jsonq = '', ...varargs) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}json-edit-xhr-request`,
+ jsonq
+ );
+ const xhrInstances = new WeakMap();
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const extraArgs = safe.parseVarargs(varargs);
+ const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
+ self.XMLHttpRequest = class extends self.XMLHttpRequest {
+ open(method, url, ...args) {
+ const xhrDetails = { method, url };
+ const matched = propNeedles.size === 0 ||
+ matchObjectPropertiesFn(propNeedles, xhrDetails);
+ if ( matched ) {
+ if ( safe.logLevel > 1 && Array.isArray(matched) ) {
+ safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`);
+ }
+ xhrInstances.set(this, xhrDetails);
+ }
+ return super.open(method, url, ...args);
+ }
+ send(body) {
+ const xhrDetails = xhrInstances.get(this);
+ if ( xhrDetails ) {
+ body = this.#filterBody(body) || body;
+ }
+ super.send(body);
+ }
+ #filterBody(body) {
+ if ( typeof body !== 'string' ) { return; }
+ let data;
+ try { data = safe.JSON_parse(body); }
+ catch { }
+ if ( data instanceof Object === false ) { return; }
+ const objAfter = jsonp.apply(data);
+ if ( objAfter === undefined ) { return; }
+ body = safe.JSON_stringify(objAfter);
+ safe.uboLog(logPrefix, 'Edited');
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `After edit:\n${body}`);
+ }
+ return body;
+ }
+ };
+}
+registerScriptlet(jsonEditXhrRequestFn, {
+ name: 'json-edit-xhr-request.fn',
+ dependencies: [
+ JSONPath,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet json-edit-xhr-request.js
+ *
+ * @description
+ * Edit the object sent as the body in a XHR instance.
+ * Properties can only be removed.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function jsonEditXhrRequest(jsonq = '', ...args) {
+ jsonEditXhrRequestFn(false, jsonq, ...args);
+}
+registerScriptlet(jsonEditXhrRequest, {
+ name: 'json-edit-xhr-request.js',
+ dependencies: [
+ jsonEditXhrRequestFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-json-edit-xhr-request.js
+ *
+ * @description
+ * Edit the object sent as the body in a XHR instance.
+ * Properties can be assigned new values.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function trustedJsonEditXhrRequest(jsonq = '', ...args) {
+ jsonEditXhrRequestFn(true, jsonq, ...args);
+}
+registerScriptlet(trustedJsonEditXhrRequest, {
+ name: 'trusted-json-edit-xhr-request.js',
+ requiresTrust: true,
+ dependencies: [
+ jsonEditXhrRequestFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function jsonEditFetchResponseFn(trusted, jsonq = '', ...varargs) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}json-edit-fetch-response`,
+ jsonq
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const extraArgs = safe.parseVarargs(varargs);
+ const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
+ proxyApplyFn('fetch', function(context) {
+ const args = context.callArgs;
+ const fetchPromise = context.reflect();
+ if ( propNeedles.size !== 0 ) {
+ const props = collateFetchArgumentsFn(...args);
+ const matched = matchObjectPropertiesFn(propNeedles, props);
+ if ( matched === undefined ) { return fetchPromise; }
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`);
+ }
+ }
+ return fetchPromise.then(responseBefore => {
+ const response = responseBefore.clone();
+ return response.json().then(obj => {
+ if ( typeof obj !== 'object' ) { return responseBefore; }
+ const objAfter = jsonp.apply(obj);
+ if ( objAfter === undefined ) { return responseBefore; }
+ safe.uboLog(logPrefix, 'Edited');
+ const responseAfter = Response.json(objAfter, {
+ status: responseBefore.status,
+ statusText: responseBefore.statusText,
+ headers: responseBefore.headers,
+ });
+ Object.defineProperties(responseAfter, {
+ ok: { value: responseBefore.ok },
+ redirected: { value: responseBefore.redirected },
+ type: { value: responseBefore.type },
+ url: { value: responseBefore.url },
+ });
+ return responseAfter;
+ }).catch(reason => {
+ safe.uboErr(logPrefix, 'Error:', reason);
+ return responseBefore;
+ });
+ }).catch(reason => {
+ safe.uboErr(logPrefix, 'Error:', reason);
+ return fetchPromise;
+ });
+ });
+}
+registerScriptlet(jsonEditFetchResponseFn, {
+ name: 'json-edit-fetch-response.fn',
+ dependencies: [
+ collateFetchArgumentsFn,
+ JSONPath,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet json-edit-fetch-response.js
+ *
+ * @description
+ * Edit the object fetched through the fetch API.
+ * Properties can only be removed.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function jsonEditFetchResponse(jsonq = '', ...args) {
+ jsonEditFetchResponseFn(false, jsonq, ...args);
+}
+registerScriptlet(jsonEditFetchResponse, {
+ name: 'json-edit-fetch-response.js',
+ dependencies: [
+ jsonEditFetchResponseFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-json-edit-fetch-response.js
+ *
+ * @description
+ * Edit the object fetched through the fetch API. The trusted version allows
+ * Properties can be assigned new values.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function trustedJsonEditFetchResponse(jsonq = '', ...args) {
+ jsonEditFetchResponseFn(true, jsonq, ...args);
+}
+registerScriptlet(trustedJsonEditFetchResponse, {
+ name: 'trusted-json-edit-fetch-response.js',
+ requiresTrust: true,
+ dependencies: [
+ jsonEditFetchResponseFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function jsonEditFetchRequestFn(trusted, jsonq = '', ...varargs) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}json-edit-fetch-request`,
+ jsonq
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const extraArgs = safe.parseVarargs(varargs);
+ const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
+ const filterBody = body => {
+ if ( typeof body !== 'string' ) { return; }
+ let data;
+ try { data = safe.JSON_parse(body); }
+ catch { }
+ if ( data instanceof Object === false ) { return; }
+ const objAfter = jsonp.apply(data);
+ if ( objAfter === undefined ) { return; }
+ return safe.JSON_stringify(objAfter);
+ }
+ const proxyHandler = context => {
+ const args = context.callArgs;
+ const [ resource, options ] = args;
+ const bodyBefore = options?.body;
+ if ( Boolean(bodyBefore) === false ) { return context.reflect(); }
+ const bodyAfter = filterBody(bodyBefore);
+ if ( bodyAfter === undefined || bodyAfter === bodyBefore ) {
+ return context.reflect();
+ }
+ if ( propNeedles.size !== 0 ) {
+ const props = collateFetchArgumentsFn(resource, options);
+ const matched = matchObjectPropertiesFn(propNeedles, props);
+ if ( matched === undefined ) { return context.reflect(); }
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`);
+ }
+ }
+ safe.uboLog(logPrefix, 'Edited');
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `After edit:\n${bodyAfter}`);
+ }
+ options.body = bodyAfter;
+ return context.reflect();
+ };
+ proxyApplyFn('fetch', proxyHandler);
+ proxyApplyFn('Request', proxyHandler);
+}
+registerScriptlet(jsonEditFetchRequestFn, {
+ name: 'json-edit-fetch-request.fn',
+ dependencies: [
+ collateFetchArgumentsFn,
+ JSONPath,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+async function editInboundElementFn(
+ trusted = false,
+ propChain = '',
+ argPosRaw = '',
+ selector = '',
+ jsonq = ''
+) {
+ if ( propChain === '' ) { return; }
+ if ( selector === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}edit-inbound-element`,
+ propChain, argPosRaw, selector, jsonq
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const argPos = parseInt(argPosRaw, 10);
+ const getElem = context => {
+ if ( argPosRaw === 'this' ) { return context.thisArg; }
+ const { callArgs } = context;
+ if ( Array.isArray(callArgs) === false ) { return; }
+ if ( isNaN(argPos) ) { return; }
+ if ( argPos >= 0 ) {
+ if ( callArgs.length <= argPos ) { return; }
+ return callArgs[argPos];
+ }
+ if ( callArgs.length < -argPos ) { return; }
+ return callArgs[callArgs.length + argPos];
+ };
+ const editElem = elem => {
+ if ( elem instanceof Element === false ) { return; }
+ if ( elem.matches(selector) === false ) { return; }
+ elem = jsonp.apply(elem);
+ if ( elem === undefined ) { return; }
+ safe.uboLog(logPrefix, 'Edited');
+ };
+ proxyApplyFn(propChain, function(context) {
+ const elem = getElem(context);
+ if ( elem !== undefined ) {
+ editElem(elem);
+ }
+ return context.reflect();
+ });
+}
+registerScriptlet(editInboundElementFn, {
+ name: 'edit-inbound-element.fn',
+ dependencies: [
+ JSONPath,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-edit-inbound-element.js
+ *
+ * @description
+ * Edit properties of an element passed as an argument to a method.
+ * Properties can be assigned new values.
+ *
+ * @param propChain
+ * Property chain of the method to trap.
+ *
+ * @param argPos
+ * 0-based position of the argument. Use negative integer for position relative
+ * to the end.
+ *
+ * @param selector
+ * The selector the element must match for the JSONPath query to be applied.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * */
+
+function trustedEditInboundElement(...args) {
+ editInboundElementFn(true, ...args);
+}
+registerScriptlet(trustedEditInboundElement, {
+ name: 'trusted-edit-inbound-element.js',
+ requiresTrust: true,
+ dependencies: [
+ editInboundElementFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet json-edit-fetch-request.js
+ *
+ * @description
+ * Edit the request body sent through the fetch API.
+ * Properties can only be removed.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when fetch() is called.
+ *
+ * */
+
+function jsonEditFetchRequest(jsonq = '', ...args) {
+ jsonEditFetchRequestFn(false, jsonq, ...args);
+}
+registerScriptlet(jsonEditFetchRequest, {
+ name: 'json-edit-fetch-request.js',
+ dependencies: [
+ jsonEditFetchRequestFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-json-edit-fetch-request.js
+ *
+ * @description
+ * Edit the request body sent through the fetch API.
+ * Properties can be assigned new values.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when fetch() is called.
+ *
+ * */
+
+function trustedJsonEditFetchRequest(jsonq = '', ...args) {
+ jsonEditFetchRequestFn(true, jsonq, ...args);
+}
+registerScriptlet(trustedJsonEditFetchRequest, {
+ name: 'trusted-json-edit-fetch-request.js',
+ requiresTrust: true,
+ dependencies: [
+ jsonEditFetchRequestFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function jsonlEditFn(jsonp, text = '', jsonExtract) {
+ const safe = safeSelf();
+ const lineSeparator = /\r?\n/.exec(text)?.[0] || '\n';
+ const linesBefore = text.split('\n');
+ const linesAfter = [];
+ for ( const lineBefore of linesBefore ) {
+ const match = jsonExtract.exec(lineBefore);
+ if ( match === null ) {
+ linesAfter.push(lineBefore);
+ continue;
+ }
+ const jsonBefore = match[1];
+ let obj;
+ try { obj = safe.JSON_parse(jsonBefore); } catch { }
+ if ( typeof obj !== 'object' || obj === null ) {
+ linesAfter.push(lineBefore);
+ continue;
+ }
+ const objAfter = jsonp.apply(obj);
+ if ( objAfter === undefined ) {
+ linesAfter.push(lineBefore);
+ continue;
+ }
+ const jsonAfter = safe.JSON_stringify(objAfter);
+ const lineAfter = [
+ lineBefore.slice(0, match.index),
+ jsonAfter,
+ lineBefore.slice(match.index + jsonBefore.length),
+ ].join('');
+ linesAfter.push(lineAfter);
+ }
+ return linesAfter.join(lineSeparator);
+}
+registerScriptlet(jsonlEditFn, {
+ name: 'jsonl-edit.fn',
+ dependencies: [
+ JSONPath,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+function jsonlEditXhrResponseFn(trusted, jsonq = '', ...varargs) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}jsonl-edit-xhr-response`,
+ jsonq
+ );
+ const xhrInstances = new WeakMap();
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const extraArgs = safe.parseVarargs(varargs);
+ const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
+ const jsonExtract = extraArgs.jsonExtract
+ ? new RegExp(extraArgs.jsonExtract)
+ : /^(.*)$/;
+ self.XMLHttpRequest = class extends self.XMLHttpRequest {
+ open(method, url, ...args) {
+ const xhrDetails = { method, url };
+ const matched = propNeedles.size === 0 ||
+ matchObjectPropertiesFn(propNeedles, xhrDetails);
+ if ( matched ) {
+ if ( safe.logLevel > 1 && Array.isArray(matched) ) {
+ safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`);
+ }
+ xhrInstances.set(this, xhrDetails);
+ }
+ return super.open(method, url, ...args);
+ }
+ get response() {
+ const innerResponse = super.response;
+ const xhrDetails = xhrInstances.get(this);
+ if ( xhrDetails === undefined ) {
+ return innerResponse;
+ }
+ const responseLength = typeof innerResponse === 'string'
+ ? innerResponse.length
+ : undefined;
+ if ( xhrDetails.lastResponseLength !== responseLength ) {
+ xhrDetails.response = undefined;
+ xhrDetails.lastResponseLength = responseLength;
+ }
+ if ( xhrDetails.response !== undefined ) {
+ return xhrDetails.response;
+ }
+ if ( typeof innerResponse !== 'string' ) {
+ return (xhrDetails.response = innerResponse);
+ }
+ const outerResponse = jsonlEditFn(jsonp, innerResponse, jsonExtract);
+ if ( outerResponse !== innerResponse ) {
+ safe.uboLog(logPrefix, 'Pruned');
+ }
+ return (xhrDetails.response = outerResponse);
+ }
+ get responseText() {
+ const response = this.response;
+ return typeof response !== 'string'
+ ? super.responseText
+ : response;
+ }
+ };
+}
+registerScriptlet(jsonlEditXhrResponseFn, {
+ name: 'jsonl-edit-xhr-response.fn',
+ dependencies: [
+ JSONPath,
+ jsonlEditFn,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet jsonl-edit-xhr-response.js
+ *
+ * @description
+ * Edit the objects found in a JSONL resource fetched through a XHR instance.
+ * Properties can only be removed.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function jsonlEditXhrResponse(jsonq = '', ...args) {
+ jsonlEditXhrResponseFn(false, jsonq, ...args);
+}
+registerScriptlet(jsonlEditXhrResponse, {
+ name: 'jsonl-edit-xhr-response.js',
+ dependencies: [
+ jsonlEditXhrResponseFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-jsonl-edit-xhr-response.js
+ *
+ * @description
+ * Edit the objects found in a JSONL resource fetched through a XHR instance.
+ * Properties can be assigned new values.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function trustedJsonlEditXhrResponse(jsonq = '', ...args) {
+ jsonlEditXhrResponseFn(true, jsonq, ...args);
+}
+registerScriptlet(trustedJsonlEditXhrResponse, {
+ name: 'trusted-jsonl-edit-xhr-response.js',
+ requiresTrust: true,
+ dependencies: [
+ jsonlEditXhrResponseFn,
+ ],
+});
+
+/******************************************************************************/
+/******************************************************************************/
+
+function jsonlEditFetchResponseFn(trusted, jsonq = '', ...varargs) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix(
+ `${trusted ? 'trusted-' : ''}jsonl-edit-fetch-response`,
+ jsonq
+ );
+ const jsonp = JSONPath.create(jsonq);
+ if ( jsonp.valid === false || jsonp.value !== undefined && trusted !== true ) {
+ return safe.uboLog(logPrefix, 'Bad JSONPath query');
+ }
+ const extraArgs = safe.parseVarargs(varargs);
+ const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
+ const jsonExtract = extraArgs.jsonExtract
+ ? new RegExp(extraArgs.jsonExtract)
+ : /^(.*)$/;
+ const logall = jsonq === '';
+ proxyApplyFn('fetch', function(context) {
+ const args = context.callArgs;
+ const fetchPromise = context.reflect();
+ if ( propNeedles.size !== 0 ) {
+ const props = collateFetchArgumentsFn(...args);
+ const matched = matchObjectPropertiesFn(propNeedles, props);
+ if ( matched === undefined ) { return fetchPromise; }
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`);
+ }
+ }
+ return fetchPromise.then(responseBefore => {
+ const response = responseBefore.clone();
+ return response.text().then(textBefore => {
+ if ( typeof textBefore !== 'string' ) { return textBefore; }
+ if ( logall ) {
+ safe.uboLog(logPrefix, textBefore);
+ return responseBefore;
+ }
+ const textAfter = jsonlEditFn(jsonp, textBefore, jsonExtract);
+ if ( textAfter === textBefore ) { return responseBefore; }
+ safe.uboLog(logPrefix, 'Pruned');
+ const responseAfter = new Response(textAfter, {
+ status: responseBefore.status,
+ statusText: responseBefore.statusText,
+ headers: responseBefore.headers,
+ });
+ Object.defineProperties(responseAfter, {
+ ok: { value: responseBefore.ok },
+ redirected: { value: responseBefore.redirected },
+ type: { value: responseBefore.type },
+ url: { value: responseBefore.url },
+ });
+ return responseAfter;
+ }).catch(reason => {
+ safe.uboErr(logPrefix, 'Error:', reason);
+ return responseBefore;
+ });
+ }).catch(reason => {
+ safe.uboErr(logPrefix, 'Error:', reason);
+ return fetchPromise;
+ });
+ });
+}
+registerScriptlet(jsonlEditFetchResponseFn, {
+ name: 'jsonl-edit-fetch-response.fn',
+ dependencies: [
+ collateFetchArgumentsFn,
+ JSONPath,
+ jsonlEditFn,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet jsonl-edit-fetch-response.js
+ *
+ * @description
+ * Edit the objects found in a JSONL resource fetched through the fetch API.
+ * Properties can only be removed.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function jsonlEditFetchResponse(jsonq = '', ...args) {
+ jsonlEditFetchResponseFn(false, jsonq, ...args);
+}
+registerScriptlet(jsonlEditFetchResponse, {
+ name: 'jsonl-edit-fetch-response.js',
+ dependencies: [
+ jsonlEditFetchResponseFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-jsonl-edit-fetch-response.js
+ *
+ * @description
+ * Edit the objects found in a JSONL resource fetched through the fetch API.
+ * Properties can be assigned new values.
+ *
+ * @param jsonq
+ * A uBO-flavored JSONPath query.
+ *
+ * @param [propsToMatch, value]
+ * An optional vararg detailing the arguments to match when xhr.open() is
+ * called.
+ *
+ * */
+
+function trustedJsonlEditFetchResponse(jsonq = '', ...args) {
+ jsonlEditFetchResponseFn(true, jsonq, ...args);
+}
+registerScriptlet(trustedJsonlEditFetchResponse, {
+ name: 'trusted-jsonl-edit-fetch-response.js',
+ requiresTrust: true,
+ dependencies: [
+ jsonlEditFetchResponseFn,
+ ],
+});
+
+/******************************************************************************/
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/json-prune.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/json-prune.js
new file mode 100644
index 00000000..22c5dc46
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/json-prune.js
@@ -0,0 +1,269 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import {
+ collateFetchArgumentsFn,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+} from './utils.js';
+
+import { objectPruneFn } from './object-prune.js';
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+function jsonPrune(
+ rawPrunePaths = '',
+ rawNeedlePaths = '',
+ stackNeedle = '',
+ ...varargs
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('json-prune', rawPrunePaths, rawNeedlePaths, stackNeedle);
+ const stackNeedleDetails = safe.initPattern(stackNeedle, { canNegate: true });
+ const extraArgs = safe.parseVarargs(varargs);
+ proxyApplyFn('JSON.parse', function(context) {
+ const objBefore = context.reflect();
+ if ( rawPrunePaths === '' ) {
+ safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2));
+ }
+ const objAfter = objectPruneFn(
+ objBefore,
+ rawPrunePaths,
+ rawNeedlePaths,
+ stackNeedleDetails,
+ extraArgs
+ );
+ if ( objAfter === undefined ) { return objBefore; }
+ safe.uboLog(logPrefix, 'Pruned');
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `After pruning:\n${safe.JSON_stringify(objAfter, null, 2)}`);
+ }
+ return objAfter;
+ });
+}
+registerScriptlet(jsonPrune, {
+ name: 'json-prune.js',
+ dependencies: [
+ objectPruneFn,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+function jsonPruneFetchResponse(
+ rawPrunePaths = '',
+ rawNeedlePaths = '',
+ ...varargs
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('json-prune-fetch-response', rawPrunePaths, rawNeedlePaths);
+ const extraArgs = safe.parseVarargs(varargs);
+ const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
+ const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true });
+ const logall = rawPrunePaths === '';
+ const applyHandler = function(target, thisArg, args) {
+ const fetchPromise = Reflect.apply(target, thisArg, args);
+ if ( propNeedles.size !== 0 ) {
+ const props = collateFetchArgumentsFn(...args);
+ const matched = matchObjectPropertiesFn(propNeedles, props);
+ if ( matched === undefined ) { return fetchPromise; }
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`);
+ }
+ }
+ return fetchPromise.then(responseBefore => {
+ const response = responseBefore.clone();
+ return response.json().then(objBefore => {
+ if ( typeof objBefore !== 'object' ) { return responseBefore; }
+ if ( logall ) {
+ safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2));
+ return responseBefore;
+ }
+ const objAfter = objectPruneFn(
+ objBefore,
+ rawPrunePaths,
+ rawNeedlePaths,
+ stackNeedle,
+ extraArgs
+ );
+ if ( typeof objAfter !== 'object' ) { return responseBefore; }
+ safe.uboLog(logPrefix, 'Pruned');
+ const responseAfter = Response.json(objAfter, {
+ status: responseBefore.status,
+ statusText: responseBefore.statusText,
+ headers: responseBefore.headers,
+ });
+ Object.defineProperties(responseAfter, {
+ ok: { value: responseBefore.ok },
+ redirected: { value: responseBefore.redirected },
+ type: { value: responseBefore.type },
+ url: { value: responseBefore.url },
+ });
+ return responseAfter;
+ }).catch(reason => {
+ safe.uboErr(logPrefix, 'Error:', reason);
+ return responseBefore;
+ });
+ }).catch(reason => {
+ safe.uboErr(logPrefix, 'Error:', reason);
+ return fetchPromise;
+ });
+ };
+ self.fetch = new Proxy(self.fetch, {
+ apply: applyHandler
+ });
+}
+registerScriptlet(jsonPruneFetchResponse, {
+ name: 'json-prune-fetch-response.js',
+ dependencies: [
+ collateFetchArgumentsFn,
+ matchObjectPropertiesFn,
+ objectPruneFn,
+ parsePropertiesToMatchFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+function jsonPruneXhrResponse(
+ rawPrunePaths = '',
+ rawNeedlePaths = '',
+ ...varargs
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('json-prune-xhr-response', rawPrunePaths, rawNeedlePaths);
+ const xhrInstances = new WeakMap();
+ const extraArgs = safe.parseVarargs(varargs);
+ const propNeedles = parsePropertiesToMatchFn(extraArgs.propsToMatch, 'url');
+ const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true });
+ self.XMLHttpRequest = class extends self.XMLHttpRequest {
+ open(method, url, ...args) {
+ const xhrDetails = { method, url };
+ let outcome = 'match';
+ if ( propNeedles.size !== 0 ) {
+ if ( matchObjectPropertiesFn(propNeedles, xhrDetails) === undefined ) {
+ outcome = 'nomatch';
+ }
+ }
+ if ( outcome === 'match' ) {
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Matched optional "propsToMatch", "${extraArgs.propsToMatch}"`);
+ }
+ xhrInstances.set(this, xhrDetails);
+ }
+ return super.open(method, url, ...args);
+ }
+ get response() {
+ const innerResponse = super.response;
+ const xhrDetails = xhrInstances.get(this);
+ if ( xhrDetails === undefined ) {
+ return innerResponse;
+ }
+ const responseLength = typeof innerResponse === 'string'
+ ? innerResponse.length
+ : undefined;
+ if ( xhrDetails.lastResponseLength !== responseLength ) {
+ xhrDetails.response = undefined;
+ xhrDetails.lastResponseLength = responseLength;
+ }
+ if ( xhrDetails.response !== undefined ) {
+ return xhrDetails.response;
+ }
+ let objBefore;
+ if ( typeof innerResponse === 'object' ) {
+ objBefore = innerResponse;
+ } else if ( typeof innerResponse === 'string' ) {
+ try {
+ objBefore = safe.JSON_parse(innerResponse);
+ } catch {
+ }
+ }
+ if ( typeof objBefore !== 'object' ) {
+ return (xhrDetails.response = innerResponse);
+ }
+ const objAfter = objectPruneFn(
+ objBefore,
+ rawPrunePaths,
+ rawNeedlePaths,
+ stackNeedle,
+ extraArgs
+ );
+ let outerResponse;
+ if ( typeof objAfter === 'object' ) {
+ outerResponse = typeof innerResponse === 'string'
+ ? safe.JSON_stringify(objAfter)
+ : objAfter;
+ safe.uboLog(logPrefix, 'Pruned');
+ } else {
+ outerResponse = innerResponse;
+ }
+ return (xhrDetails.response = outerResponse);
+ }
+ get responseText() {
+ const response = this.response;
+ return typeof response !== 'string'
+ ? super.responseText
+ : response;
+ }
+ };
+}
+registerScriptlet(jsonPruneXhrResponse, {
+ name: 'json-prune-xhr-response.js',
+ dependencies: [
+ matchObjectPropertiesFn,
+ objectPruneFn,
+ parsePropertiesToMatchFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+// There is still code out there which uses `eval` in lieu of `JSON.parse`.
+
+function evaldataPrune(
+ rawPrunePaths = '',
+ rawNeedlePaths = ''
+) {
+ proxyApplyFn('eval', function(context) {
+ const before = context.reflect();
+ if ( typeof before !== 'object' ) { return before; }
+ if ( before === null ) { return null; }
+ const after = objectPruneFn(before, rawPrunePaths, rawNeedlePaths);
+ return after || before;
+ });
+}
+registerScriptlet(evaldataPrune, {
+ name: 'evaldata-prune.js',
+ dependencies: [
+ objectPruneFn,
+ proxyApplyFn,
+ ],
+});
+
+/******************************************************************************/
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/localstorage.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/localstorage.js
new file mode 100644
index 00000000..69c03709
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/localstorage.js
@@ -0,0 +1,210 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { getSafeCookieValuesFn } from './cookie.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+export function getAllLocalStorageFn(which = 'localStorage') {
+ const storage = self[which];
+ const out = [];
+ for ( let i = 0; i < storage.length; i++ ) {
+ const key = storage.key(i);
+ const value = storage.getItem(key);
+ return { key, value };
+ }
+ return out;
+}
+registerScriptlet(getAllLocalStorageFn, {
+ name: 'get-all-local-storage.fn',
+});
+
+/******************************************************************************/
+
+export function setLocalStorageItemFn(
+ which = 'local',
+ trusted = false,
+ key = '',
+ value = '',
+ options = {}
+) {
+ if ( key === '' ) { return; }
+
+ // For increased compatibility with AdGuard
+ if ( value === 'emptyArr' ) {
+ value = '[]';
+ } else if ( value === 'emptyObj' ) {
+ value = '{}';
+ }
+
+ const trustedValues = [
+ '',
+ 'undefined', 'null',
+ '{}', '[]', '""',
+ '$remove$',
+ ...getSafeCookieValuesFn(),
+ ];
+
+ if ( trusted ) {
+ if ( value.includes('$now$') ) {
+ value = value.replaceAll('$now$', Date.now());
+ }
+ if ( value.includes('$currentDate$') ) {
+ value = value.replaceAll('$currentDate$', `${Date()}`);
+ }
+ if ( value.includes('$currentISODate$') ) {
+ value = value.replaceAll('$currentISODate$', (new Date()).toISOString());
+ }
+ } else {
+ const normalized = value.toLowerCase();
+ const match = /^("?)(.+)\1$/.exec(normalized);
+ const unquoted = match && match[2] || normalized;
+ if ( trustedValues.includes(unquoted) === false ) {
+ if ( /^-?\d+$/.test(unquoted) === false ) { return; }
+ const n = parseInt(unquoted, 10) || 0;
+ if ( n < -32767 || n > 32767 ) { return; }
+ }
+ }
+
+ let modified = false;
+
+ try {
+ const storage = self[`${which}Storage`];
+ if ( value === '$remove$' ) {
+ const safe = safeSelf();
+ const pattern = safe.patternToRegex(key, undefined, true );
+ const toRemove = [];
+ for ( let i = 0, n = storage.length; i < n; i++ ) {
+ const key = storage.key(i);
+ if ( pattern.test(key) ) { toRemove.push(key); }
+ }
+ modified = toRemove.length !== 0;
+ for ( const key of toRemove ) {
+ storage.removeItem(key);
+ }
+ } else {
+
+ const before = storage.getItem(key);
+ const after = `${value}`;
+ modified = after !== before;
+ if ( modified ) {
+ storage.setItem(key, after);
+ }
+ }
+ } catch {
+ }
+
+ if ( modified && typeof options.reload === 'number' ) {
+ setTimeout(( ) => { window.location.reload(); }, options.reload);
+ }
+}
+registerScriptlet(setLocalStorageItemFn, {
+ name: 'set-local-storage-item.fn',
+ dependencies: [
+ getSafeCookieValuesFn,
+ safeSelf,
+ ],
+});
+
+/*******************************************************************************
+ *
+ * set-local-storage-item.js
+ * set-session-storage-item.js
+ *
+ * Set a local/session storage entry to a specific, allowed value.
+ *
+ * Reference:
+ * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-local-storage-item.js
+ * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-session-storage-item.js
+ *
+ **/
+
+export function setLocalStorageItem(key = '', value = '', ...varargs) {
+ const safe = safeSelf();
+ const options = safe.parseVarargs(varargs)
+ setLocalStorageItemFn('local', false, key, value, options);
+}
+registerScriptlet(setLocalStorageItem, {
+ name: 'set-local-storage-item.js',
+ world: 'ISOLATED',
+ dependencies: [
+ safeSelf,
+ setLocalStorageItemFn,
+ ],
+});
+
+export function setSessionStorageItem(key = '', value = '', ...varargs) {
+ const safe = safeSelf();
+ const options = safe.parseVarargs(varargs)
+ setLocalStorageItemFn('session', false, key, value, options);
+}
+registerScriptlet(setSessionStorageItem, {
+ name: 'set-session-storage-item.js',
+ world: 'ISOLATED',
+ dependencies: [
+ safeSelf,
+ setLocalStorageItemFn,
+ ],
+});
+
+/*******************************************************************************
+ *
+ * trusted-set-local-storage-item.js
+ *
+ * Set a local storage entry to an arbitrary value.
+ *
+ * Reference:
+ * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-set-local-storage-item.js
+ *
+ **/
+
+export function trustedSetLocalStorageItem(key = '', value = '', ...varargs) {
+ const safe = safeSelf();
+ const options = safe.parseVarargs(varargs)
+ setLocalStorageItemFn('local', true, key, value, options);
+}
+registerScriptlet(trustedSetLocalStorageItem, {
+ name: 'trusted-set-local-storage-item.js',
+ requiresTrust: true,
+ world: 'ISOLATED',
+ dependencies: [
+ safeSelf,
+ setLocalStorageItemFn,
+ ],
+});
+
+export function trustedSetSessionStorageItem(key = '', value = '', ...varargs) {
+ const safe = safeSelf();
+ const options = safe.parseVarargs(varargs)
+ setLocalStorageItemFn('session', true, key, value, options);
+}
+registerScriptlet(trustedSetSessionStorageItem, {
+ name: 'trusted-set-session-storage-item.js',
+ requiresTrust: true,
+ world: 'ISOLATED',
+ dependencies: [
+ safeSelf,
+ setLocalStorageItemFn,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/noeval.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/noeval.js
new file mode 100644
index 00000000..d66db532
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/noeval.js
@@ -0,0 +1,58 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+function noEvalIf(
+ needle = ''
+) {
+ if ( typeof needle !== 'string' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('noeval-if', needle);
+ const reNeedle = safe.patternToRegex(needle);
+ proxyApplyFn('eval', function(context) {
+ const { callArgs } = context;
+ const a = String(callArgs[0]);
+ if ( needle !== '' && reNeedle.test(a) ) {
+ safe.uboLog(logPrefix, 'Prevented:\n', a);
+ return;
+ }
+ if ( needle === '' || safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, 'Not prevented:\n', a);
+ }
+ return context.reflect();
+ });
+}
+registerScriptlet(noEvalIf, {
+ name: 'noeval-if.js',
+ aliases: [
+ 'prevent-eval-if.js',
+ ],
+ dependencies: [
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/object-prune.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/object-prune.js
new file mode 100644
index 00000000..537e2b20
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/object-prune.js
@@ -0,0 +1,274 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { matchesStackTraceFn } from './stack-trace.js';
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+function objectFindOwnerFn(
+ root,
+ path,
+ prune = false
+) {
+ const safe = safeSelf();
+ let owner = root;
+ let chain = path;
+ for (;;) {
+ if ( typeof owner !== 'object' || owner === null ) { return false; }
+ const pos = chain.indexOf('.');
+ if ( pos === -1 ) {
+ if ( prune === false ) {
+ return safe.Object_hasOwn(owner, chain);
+ }
+ let modified = false;
+ if ( chain === '*' ) {
+ for ( const key in owner ) {
+ if ( safe.Object_hasOwn(owner, key) === false ) { continue; }
+ delete owner[key];
+ modified = true;
+ }
+ } else if ( safe.Object_hasOwn(owner, chain) ) {
+ delete owner[chain];
+ modified = true;
+ }
+ return modified;
+ }
+ const prop = chain.slice(0, pos);
+ const next = chain.slice(pos + 1);
+ let found = false;
+ if ( prop === '[-]' && Array.isArray(owner) ) {
+ let i = owner.length;
+ while ( i-- ) {
+ if ( objectFindOwnerFn(owner[i], next) === false ) { continue; }
+ owner.splice(i, 1);
+ found = true;
+ }
+ return found;
+ }
+ if ( prop === '{-}' && owner instanceof Object ) {
+ for ( const key of Object.keys(owner) ) {
+ if ( objectFindOwnerFn(owner[key], next) === false ) { continue; }
+ delete owner[key];
+ found = true;
+ }
+ return found;
+ }
+ if (
+ prop === '[]' && Array.isArray(owner) ||
+ prop === '{}' && owner instanceof Object ||
+ prop === '*' && owner instanceof Object
+ ) {
+ for ( const key of Object.keys(owner) ) {
+ if (objectFindOwnerFn(owner[key], next, prune) === false ) { continue; }
+ found = true;
+ }
+ return found;
+ }
+ if ( safe.Object_hasOwn(owner, prop) === false ) { return false; }
+ owner = owner[prop];
+ chain = chain.slice(pos + 1);
+ }
+}
+registerScriptlet(objectFindOwnerFn, {
+ name: 'object-find-owner.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+// When no "prune paths" argument is provided, the scriptlet is
+// used for logging purpose and the "needle paths" argument is
+// used to filter logging output.
+//
+// https://github.com/uBlockOrigin/uBlock-issues/issues/1545
+// - Add support for "remove everything if needle matches" case
+
+export function objectPruneFn(
+ obj,
+ rawPrunePaths,
+ rawNeedlePaths,
+ stackNeedleDetails = { matchAll: true },
+ extraArgs = {}
+) {
+ if ( typeof rawPrunePaths !== 'string' ) { return; }
+ const safe = safeSelf();
+ const prunePaths = rawPrunePaths !== ''
+ ? safe.String_split.call(rawPrunePaths, / +/)
+ : [];
+ const needlePaths = prunePaths.length !== 0 && rawNeedlePaths !== ''
+ ? safe.String_split.call(rawNeedlePaths, / +/)
+ : [];
+ if ( stackNeedleDetails.matchAll !== true ) {
+ if ( matchesStackTraceFn(stackNeedleDetails, extraArgs.logstack) === false ) {
+ return;
+ }
+ }
+ if ( objectPruneFn.mustProcess === undefined ) {
+ objectPruneFn.mustProcess = (root, needlePaths) => {
+ for ( const needlePath of needlePaths ) {
+ if ( objectFindOwnerFn(root, needlePath) === false ) {
+ return false;
+ }
+ }
+ return true;
+ };
+ }
+ if ( prunePaths.length === 0 ) { return; }
+ let outcome = 'nomatch';
+ if ( objectPruneFn.mustProcess(obj, needlePaths) ) {
+ for ( const path of prunePaths ) {
+ if ( objectFindOwnerFn(obj, path, true) ) {
+ outcome = 'match';
+ }
+ }
+ }
+ if ( outcome === 'match' ) { return obj; }
+}
+registerScriptlet(objectPruneFn, {
+ name: 'object-prune.fn',
+ dependencies: [
+ matchesStackTraceFn,
+ objectFindOwnerFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+function trustedPruneInboundObject(
+ entryPoint = '',
+ argPos = '',
+ rawPrunePaths = '',
+ rawNeedlePaths = '',
+ ...varargs
+) {
+ if ( entryPoint === '' ) { return; }
+ let context = globalThis;
+ let prop = entryPoint;
+ for (;;) {
+ const pos = prop.indexOf('.');
+ if ( pos === -1 ) { break; }
+ context = context[prop.slice(0, pos)];
+ if ( context instanceof Object === false ) { return; }
+ prop = prop.slice(pos+1);
+ }
+ if ( typeof context[prop] !== 'function' ) { return; }
+ const argIndex = parseInt(argPos);
+ if ( isNaN(argIndex) ) { return; }
+ if ( argIndex < 1 ) { return; }
+ const safe = safeSelf();
+ const extraArgs = safe.parseVarargs(varargs);
+ const needlePaths = [];
+ if ( rawPrunePaths !== '' ) {
+ needlePaths.push(...safe.String_split.call(rawPrunePaths, / +/));
+ }
+ if ( rawNeedlePaths !== '' ) {
+ needlePaths.push(...safe.String_split.call(rawNeedlePaths, / +/));
+ }
+ const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true });
+ const mustProcess = root => {
+ for ( const needlePath of needlePaths ) {
+ if ( objectFindOwnerFn(root, needlePath) === false ) {
+ return false;
+ }
+ }
+ return true;
+ };
+ context[prop] = new Proxy(context[prop], {
+ apply: function(target, thisArg, args) {
+ const targetArg = argIndex <= args.length
+ ? args[argIndex-1]
+ : undefined;
+ if ( targetArg instanceof Object && mustProcess(targetArg) ) {
+ let objBefore = targetArg;
+ if ( extraArgs.dontOverwrite ) {
+ try {
+ objBefore = safe.JSON_parse(safe.JSON_stringify(targetArg));
+ } catch {
+ objBefore = undefined;
+ }
+ }
+ if ( objBefore !== undefined ) {
+ const objAfter = objectPruneFn(
+ objBefore,
+ rawPrunePaths,
+ rawNeedlePaths,
+ stackNeedle,
+ extraArgs
+ );
+ args[argIndex-1] = objAfter || objBefore;
+ }
+ }
+ return Reflect.apply(target, thisArg, args);
+ },
+ });
+}
+registerScriptlet(trustedPruneInboundObject, {
+ name: 'trusted-prune-inbound-object.js',
+ requiresTrust: true,
+ dependencies: [
+ objectFindOwnerFn,
+ objectPruneFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+function trustedPruneOutboundObject(
+ propChain = '',
+ rawPrunePaths = '',
+ rawNeedlePaths = '',
+ ...varargs
+) {
+ if ( propChain === '' ) { return; }
+ const safe = safeSelf();
+ const extraArgs = safe.parseVarargs(varargs);
+ proxyApplyFn(propChain, function(context) {
+ const objBefore = context.reflect();
+ if ( objBefore instanceof Object === false ) { return objBefore; }
+ const objAfter = objectPruneFn(
+ objBefore,
+ rawPrunePaths,
+ rawNeedlePaths,
+ { matchAll: true },
+ extraArgs
+ );
+ return objAfter || objBefore;
+ });
+}
+registerScriptlet(trustedPruneOutboundObject, {
+ name: 'trusted-prune-outbound-object.js',
+ requiresTrust: true,
+ dependencies: [
+ objectPruneFn,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/parse-replace.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/parse-replace.js
new file mode 100644
index 00000000..604644f4
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/parse-replace.js
@@ -0,0 +1,54 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { ArglistParser } from './shared.js';
+import { registerScriptlet } from './base.js';
+
+/******************************************************************************/
+
+export function parseReplaceFn(s) {
+ if ( s.charCodeAt(0) !== 0x2F /* / */ ) { return; }
+ const parser = new ArglistParser('/');
+ parser.nextArg(s, 1);
+ let pattern = s.slice(parser.argBeg, parser.argEnd);
+ if ( parser.transform ) {
+ pattern = parser.normalizeArg(pattern);
+ }
+ if ( pattern === '' ) { return; }
+ parser.nextArg(s, parser.separatorEnd);
+ let replacement = s.slice(parser.argBeg, parser.argEnd);
+ if ( parser.separatorEnd === parser.separatorBeg ) { return; }
+ if ( parser.transform ) {
+ replacement = parser.normalizeArg(replacement);
+ }
+ const flags = s.slice(parser.separatorEnd);
+ try {
+ return { re: new RegExp(pattern, flags), replacement };
+ } catch {
+ }
+}
+registerScriptlet(parseReplaceFn, {
+ name: 'parse-replace.fn',
+ dependencies: [
+ ArglistParser,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-addeventlistener.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-addeventlistener.js
new file mode 100644
index 00000000..1321edac
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-addeventlistener.js
@@ -0,0 +1,160 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { runAt } from './run-at.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+/**
+ * @scriptlet prevent-addEventListener
+ *
+ * @description
+ * Conditionally prevent execution of the callback function passed to native
+ * addEventListener method. With no parameters, all calls to addEventListener
+ * will be shown in the logger.
+ *
+ * @param [type]
+ * The type of the event to prevent. The pattern can be a plain string, or a
+ * regex to specify more than one event type.
+ *
+ * @param [pattern]
+ * A pattern to match against the stringified callback. The pattern can be a
+ * plain string, or a regex.
+ *
+ * @param [runat, value]
+ * An optional vararg which tell the scriptlet when the prevention should
+ * start. Values correspond to `readyState`: loading, interactive, complete.
+ *
+ * @param [protect, 1]
+ * An optional vararg which tells the scriptlet whether the prevention should
+ * be protected, i.e. not overwritten by other code.
+ *
+ * */
+
+function preventAddEventListener(
+ type = '',
+ pattern = '',
+ ...varargs
+) {
+ const safe = safeSelf();
+ const extraArgs = safe.parseVarargs(varargs);
+ const logPrefix = safe.makeLogPrefix('prevent-addEventListener', type, pattern);
+ const reType = safe.patternToRegex(type, undefined, true);
+ const rePattern = safe.patternToRegex(pattern);
+ const targetSelector = extraArgs.elements || undefined;
+ const elementMatches = elem => {
+ if ( targetSelector === 'window' ) { return elem === window; }
+ if ( targetSelector === 'document' ) { return elem === document; }
+ if ( elem && elem.matches && elem.matches(targetSelector) ) { return true; }
+ const elems = Array.from(document.querySelectorAll(targetSelector));
+ return elems.includes(elem);
+ };
+ const elementDetails = elem => {
+ if ( elem instanceof Window ) { return 'window'; }
+ if ( elem instanceof Document ) { return 'document'; }
+ if ( elem instanceof Element === false ) { return '?'; }
+ const parts = [];
+ // https://github.com/uBlockOrigin/uAssets/discussions/17907#discussioncomment-9871079
+ const id = String(elem.id);
+ if ( id !== '' ) { parts.push(`#${CSS.escape(id)}`); }
+ for ( let i = 0; i < elem.classList.length; i++ ) {
+ parts.push(`.${CSS.escape(elem.classList.item(i))}`);
+ }
+ for ( let i = 0; i < elem.attributes.length; i++ ) {
+ const attr = elem.attributes.item(i);
+ if ( attr.name === 'id' ) { continue; }
+ if ( attr.name === 'class' ) { continue; }
+ parts.push(`[${CSS.escape(attr.name)}="${attr.value}"]`);
+ }
+ return parts.join('');
+ };
+ const shouldPrevent = (thisArg, type, handler) => {
+ const matchesType = safe.RegExp_test(reType, type);
+ const matchesHandler = safe.RegExp_test(rePattern, handler);
+ const matchesEither = matchesType || matchesHandler;
+ const matchesBoth = matchesType && matchesHandler;
+ if ( safe.logLevel > 1 && matchesEither ) {
+ debugger; // eslint-disable-line no-debugger
+ }
+ if ( matchesBoth && targetSelector !== undefined ) {
+ if ( elementMatches(thisArg) === false ) { return false; }
+ }
+ return matchesBoth;
+ };
+ const proxyFn = function(context) {
+ const { callArgs, thisArg } = context;
+ let t, h;
+ try {
+ t = String(callArgs[0]);
+ if ( typeof callArgs[1] === 'function' ) {
+ h = String(safe.Function_toString(callArgs[1]));
+ } else if ( typeof callArgs[1] === 'object' && callArgs[1] !== null ) {
+ if ( typeof callArgs[1].handleEvent === 'function' ) {
+ h = String(safe.Function_toString(callArgs[1].handleEvent));
+ }
+ } else {
+ h = String(callArgs[1]);
+ }
+ } catch {
+ }
+ if ( type === '' && pattern === '' ) {
+ safe.uboLog(logPrefix, `Called: ${t}\n${h}\n${elementDetails(thisArg)}`);
+ } else if ( shouldPrevent(thisArg, t, h) ) {
+ return safe.uboLog(logPrefix, `Prevented: ${t}\n${h}\n${elementDetails(thisArg)}`);
+ }
+ return context.reflect();
+ };
+ const protect = owner => {
+ const { addEventListener } = owner;
+ Object.defineProperty(owner, 'addEventListener', {
+ set() { },
+ get() { return addEventListener; }
+ });
+ };
+ runAt(( ) => {
+ proxyApplyFn('EventTarget.prototype.addEventListener', proxyFn);
+ if ( extraArgs.protect ) { protect(EventTarget.prototype); }
+ if ( Object.hasOwn(document, 'addEventListener') ) {
+ proxyApplyFn('document.addEventListener', proxyFn);
+ if ( extraArgs.protect ) { protect(document); }
+ }
+ if ( Object.hasOwn(window, 'addEventListener') ) {
+ proxyApplyFn('window.addEventListener', proxyFn);
+ if ( extraArgs.protect ) { protect(window); }
+ }
+ }, extraArgs.runAt);
+}
+registerScriptlet(preventAddEventListener , {
+ name: 'prevent-addEventListener.js',
+ aliases: [
+ 'addEventListener-defuser.js',
+ 'aeld.js',
+ ],
+ dependencies: [
+ proxyApplyFn,
+ runAt,
+ safeSelf,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-bab.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-bab.js
new file mode 100644
index 00000000..22af8059
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-bab.js
@@ -0,0 +1,101 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+function preventBab() {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('prevent-bab');
+ const signatures = [
+ [ 'blockadblock' ],
+ [ 'babasbm' ],
+ [ /getItem\('babn'\)/ ],
+ [
+ 'getElementById',
+ 'String.fromCharCode',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
+ 'charAt',
+ 'DOMContentLoaded',
+ 'AdBlock',
+ 'addEventListener',
+ 'doScroll',
+ 'fromCharCode',
+ '<<2|r>>4',
+ 'sessionStorage',
+ 'clientWidth',
+ 'localStorage',
+ 'Math',
+ 'random',
+ ],
+ ];
+ const check = function(s) {
+ if ( typeof s !== 'string' ) { return false; }
+ for ( const tokens of signatures ) {
+ let match = 0;
+ for ( const token of tokens ) {
+ const hit = token instanceof RegExp
+ ? token.test(s)
+ : s.includes(token);
+ if ( hit ) { match += 1; }
+ }
+ if ( (match / tokens.length) >= 0.8 ) { return true; }
+ }
+ return false;
+ };
+ proxyApplyFn('eval', function(context) {
+ const a = context.callArgs[0];
+ if ( !check(a) ) {
+ return context.reflect();
+ }
+ safe.uboLog(logPrefix, 'Prevented');
+ if ( document.body ) {
+ document.body.style.removeProperty('visibility');
+ }
+ const el = document.getElementById('babasbmsgx');
+ if ( el ) {
+ el.parentNode.removeChild(el);
+ }
+ });
+ proxyApplyFn('setTimeout', function(context) {
+ const { callArgs } = context;
+ const a = callArgs[0];
+ if ( typeof a === 'string' && /\.bab_elementid.$/.test(a) ) {
+ callArgs[0] = ( ) => { };
+ safe.uboLog(logPrefix, 'Prevented');
+ }
+ return context.reflect();
+ });
+}
+registerScriptlet(preventBab, {
+ name: 'prevent-bab.js',
+ aliases: [
+ 'bab-defuser.js',
+ 'nobab.js',
+ ],
+ dependencies: [
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-clipboard-write.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-clipboard-write.js
new file mode 100644
index 00000000..a3e4b54f
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-clipboard-write.js
@@ -0,0 +1,153 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { runAt } from './run-at.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+/**
+ * @scriptlet prevent-clipboard-write
+ *
+ * @description
+ * Prevent the clipboard from being overwritten.
+ *
+ * @param matches
+ * A pattern or regex to match against the text for the prevention to occur.
+ *
+ * @param excludeMatches
+ * Optional. A vararg to be used as a pattern or regex to match against the
+ * text for the prevention to NOT occur.
+ *
+ * @param domAlert
+ * Optional. A vararg to be used to alert the user in case a clipboard write
+ * operation was prevented.
+ *
+ * @example
+ * ##+js(prevent-clipboard-write, /^bash <<, , domAlert, Clickfix attempt defused)
+ *
+ * */
+
+function preventClipboardWrite(matches = '', ...varargs) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('prevent-clipboard-write');
+ const pattern = safe.initPattern(matches);
+ const extraArgs = safe.parseVarargs(varargs);
+ const excludePattern = extraArgs.excludeMatches &&
+ safe.initPattern(extraArgs.excludeMatches);
+ const htmlTemplate = [
+ '
',
+ '${warning}\n',
+ '',
+ '
',
+ ].join('');
+ const domAlert = clipboardText => {
+ const doc = document;
+ const domAlert = extraArgs.domAlert.replace(/\\n/g, '\n');
+ let html;
+ if ( domAlert.includes('${text}') ) {
+ const code = doc.createElement('code');
+ const styles = [
+ 'background-color: #ddc',
+ 'display: inline-block',
+ 'font-family: monospace',
+ 'font-size: 100%',
+ 'max-height: 8em',
+ 'overflow: auto',
+ 'padding: 0.25em',
+ 'user-select: all',
+ 'width: 100%;',
+ 'word-break: break-all'
+ ];
+ if ( Boolean(extraArgs.selectable ?? true) === false ) {
+ styles.push('user-select: none');
+ }
+ code.style = styles.join(';');
+ code.textContent = clipboardText;
+ html = htmlTemplate.replace('${warning}',
+ domAlert.replace('${text}', code.outerHTML)
+ );
+ } else {
+ html = htmlTemplate.replace('${warning}', domAlert);
+ }
+ if ( currentAlert ) { currentAlert.remove(); }
+ const domParser = new DOMParser();
+ const fragment = domParser.parseFromString(html, 'text/html');
+ currentAlert = fragment.querySelector('div');
+ const button = currentAlert.querySelector('button');
+ button.addEventListener('click', ( ) => {
+ if ( currentAlert === null ) { return; }
+ currentAlert.remove();
+ currentAlert = null;
+ });
+ currentAlert.setAttribute('popover', 'manual');
+ doc.documentElement.append(currentAlert);
+ if ( typeof currentAlert.showPopover === 'function' ) {
+ currentAlert.showPopover();
+ }
+ };
+ let currentAlert = null;
+ const prevent = text => {
+ if ( typeof text !== 'string' ) { return; }
+ text = text.trim();
+ if ( safe.testPattern(pattern, text) !== true ) { return; }
+ if ( extraArgs.excludeMatches ) {
+ if ( safe.testPattern(excludePattern, text) ) { return; }
+ }
+ if ( extraArgs.domAlert ) {
+ domAlert(text);
+ }
+ safe.uboLog(logPrefix, 'Prevented:\n\t', text);
+ return true;
+ };
+ const installTraps = ( ) => {
+ proxyApplyFn('navigator.clipboard.writeText', async function(context) {
+ const text = `${context.callArgs[0]}`;
+ if ( prevent(text) ) { return; }
+ return context.reflect();
+ }, { skipToString: true });
+ proxyApplyFn('document.execCommand', function(context) {
+ const { callArgs } = context;
+ if ( callArgs[0] === 'copy' || callArgs[0] === 'cut' ) {
+ const text = document.getSelection()?.toString();
+ if ( prevent(text) ) { return true; }
+ }
+ return context.reflect();
+ }, { skipToString: true });
+ };
+ runAt(( ) => {
+ self.document.addEventListener('mousemove', installTraps, {
+ once: true,
+ capture: true,
+ });
+ }, 'interactive')
+}
+registerScriptlet(preventClipboardWrite, {
+ name: 'prevent-clipboard-write.js',
+ requiresTrust: true,
+ dependencies: [
+ proxyApplyFn,
+ runAt,
+ safeSelf,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-dialog.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-dialog.js
new file mode 100644
index 00000000..fbb86ef9
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-dialog.js
@@ -0,0 +1,73 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2025-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/**
+ * @scriptlet prevent-dialog
+ *
+ * @description
+ * Programmatically close `dialog` elements.
+ *
+ * @param [selector]
+ * Optional. The dialog element must matches `dialog{selector}` for the
+ * prevention to take place.
+ *
+ * @usage:
+ * example.com##+js(prevent-dialog)
+ *
+ * */
+
+export function preventDialog(
+ selector = '',
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('prevent-dialog', selector);
+ const prevent = ( ) => {
+ debouncer = undefined;
+ const elems = document.querySelectorAll(`dialog${selector}`);
+ for ( const elem of elems ) {
+ if ( typeof elem.close !== 'function' ) { continue; }
+ if ( elem.open === false ) { continue; }
+ elem.close();
+ safe.uboLog(logPrefix, 'Closed');
+ }
+ };
+ let debouncer;
+ const observer = new MutationObserver(( ) => {
+ if ( debouncer !== undefined ) { return; }
+ debouncer = requestAnimationFrame(prevent);
+ });
+ observer.observe(document, {
+ attributes: true,
+ childList: true,
+ subtree: true,
+ });
+}
+registerScriptlet(preventDialog, {
+ name: 'prevent-dialog.js',
+ dependencies: [
+ safeSelf,
+ ],
+ world: 'ISOLATED',
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-fetch.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-fetch.js
new file mode 100644
index 00000000..d636859b
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-fetch.js
@@ -0,0 +1,198 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import {
+ collateFetchArgumentsFn,
+ generateContentFn,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+} from './utils.js';
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+function preventFetchFn(
+ trusted = false,
+ propsToMatch = '',
+ responseBody = '',
+ responseType = '',
+ ...varargs
+) {
+ const safe = safeSelf();
+ const setTimeout = self.setTimeout;
+ const scriptletName = `${trusted ? 'trusted-' : ''}prevent-fetch`;
+ const logPrefix = safe.makeLogPrefix(
+ scriptletName,
+ propsToMatch,
+ responseBody,
+ responseType
+ );
+ const extraArgs = safe.parseVarargs(varargs);
+ const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
+ const validResponseProps = {
+ ok: [ false, true ],
+ status: [ 403 ],
+ statusText: [ '', 'Not Found' ],
+ type: [ 'basic', 'cors', 'default', 'error', 'opaque' ],
+ };
+ const responseProps = {
+ statusText: { value: 'OK' },
+ };
+ const responseHeaders = {};
+ if ( /^\{.*\}$/.test(responseType) ) {
+ try {
+ Object.entries(JSON.parse(responseType)).forEach(([ p, v ]) => {
+ if ( p === 'headers' && trusted ) {
+ Object.assign(responseHeaders, v);
+ return;
+ }
+ if ( validResponseProps[p] === undefined ) { return; }
+ if ( validResponseProps[p].includes(v) === false ) { return; }
+ responseProps[p] = { value: v };
+ });
+ }
+ catch { }
+ } else if ( responseType !== '' ) {
+ if ( validResponseProps.type.includes(responseType) ) {
+ responseProps.type = { value: responseType };
+ }
+ }
+ proxyApplyFn('fetch', function fetch(context) {
+ const { callArgs } = context;
+ const details = collateFetchArgumentsFn(...callArgs);
+ if ( safe.logLevel > 1 || propsToMatch === '' && responseBody === '' ) {
+ const out = Array.from(Object.entries(details)).map(a => `${a[0]}:${a[1]}`);
+ safe.uboLog(logPrefix, `Called: ${out.join('\n')}`);
+ }
+ if ( propsToMatch === '' && responseBody === '' ) {
+ return context.reflect();
+ }
+ const matched = matchObjectPropertiesFn(propNeedles, details);
+ if ( matched === undefined || matched.length === 0 ) {
+ return context.reflect();
+ }
+ return Promise.resolve(generateContentFn(trusted, responseBody)).then(text => {
+ safe.uboLog(logPrefix, `Prevented with response "${text}"`);
+ const headers = Object.assign({}, responseHeaders);
+ if ( headers['content-length'] === undefined ) {
+ headers['content-length'] = text.length;
+ }
+ const response = new Response(text, { headers });
+ const props = Object.assign(
+ { url: { value: details.url } },
+ responseProps
+ );
+ safe.Object_defineProperties(response, props);
+ if ( extraArgs.throttle ) {
+ return new Promise(resolve => {
+ setTimeout(( ) => { resolve(response); }, extraArgs.throttle);
+ });
+ }
+ return response;
+ });
+ });
+}
+registerScriptlet(preventFetchFn, {
+ name: 'prevent-fetch.fn',
+ dependencies: [
+ collateFetchArgumentsFn,
+ generateContentFn,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet prevent-fetch
+ *
+ * @description
+ * Prevent a fetch() call from making a network request to a remote server.
+ *
+ * @param propsToMatch
+ * The fetch arguments to match for the prevention to be triggered. The
+ * untrusted flavor limits the realm of response to return to safe values.
+ *
+ * @param [responseBody]
+ * Optional. The reponse to return when the prevention occurs.
+ *
+ * @param [responseType]
+ * Optional. The response type to use when emitting a dummy response as a
+ * result of the prevention.
+ *
+ * @param [...varargs]
+ * ["throttle", n]: the time to wait in ms before returning a result.
+ *
+ * */
+
+function preventFetch(...args) {
+ preventFetchFn(false, ...args);
+}
+registerScriptlet(preventFetch, {
+ name: 'prevent-fetch.js',
+ aliases: [
+ 'no-fetch-if.js',
+ ],
+ dependencies: [
+ preventFetchFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-prevent-fetch
+ *
+ * @description
+ * Prevent a fetch() call from making a network request to a remote server.
+ *
+ * @param propsToMatch
+ * The fetch arguments to match for the prevention to be triggered.
+ *
+ * @param [responseBody]
+ * Optional. The reponse to return when the prevention occurs. The trusted
+ * flavor allows to return any response.
+ *
+ * @param [responseType]
+ * Optional. The response type to use when emitting a dummy response as a
+ * result of the prevention.
+ *
+ * @param [...varargs]
+ * ["throttle", n]: the time to wait in ms before returning a result.
+ *
+ * */
+
+function trustedPreventFetch(...args) {
+ preventFetchFn(true, ...args);
+}
+registerScriptlet(trustedPreventFetch, {
+ name: 'trusted-prevent-fetch.js',
+ requiresTrust: true,
+ dependencies: [
+ preventFetchFn,
+ ],
+});
+
+/******************************************************************************/
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-innerHTML.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-innerHTML.js
new file mode 100644
index 00000000..1e14c2ae
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-innerHTML.js
@@ -0,0 +1,132 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2025-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/**
+ * @scriptlet freeze-element-property
+ *
+ * @description
+ * Conditionally prevent assignment to an element property.
+ *
+ * @param property
+ * The name of the property to freeze.
+ *
+ * @param [selector]
+ * Optional. The element must match `selector` for the prevention to take
+ * place.
+ *
+ * @param [pattern]
+ * Optional. A pattern to match against the stringified assigned value. The
+ * pattern can be a plain string, or a regex. Prepend with `!` to reverse the
+ * match condition.
+ *
+ * */
+
+function freezeElementProperty(
+ property = '',
+ selector = '',
+ pattern = ''
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('freeze-element-property', property, selector, pattern);
+ const matcher = safe.initPattern(pattern, { canNegate: true });
+ const owner = (( ) => {
+ if ( Object.hasOwn(HTMLScriptElement.prototype, property) ) {
+ return HTMLScriptElement.prototype;
+ }
+ if ( Object.hasOwn(HTMLElement.prototype, property) ) {
+ return HTMLElement.prototype;
+ }
+ if ( Object.hasOwn(Element.prototype, property) ) {
+ return Element.prototype;
+ }
+ if ( Object.hasOwn(Node.prototype, property) ) {
+ return Node.prototype;
+ }
+ return null;
+ })();
+ if ( owner === null ) { return; }
+ const current = safe.Object_getOwnPropertyDescriptor(owner, property);
+ if ( current === undefined ) { return; }
+ const shouldPreventSet = (elem, a) => {
+ if ( selector !== '' ) {
+ if ( typeof elem.matches !== 'function' ) { return false; }
+ if ( elem.matches(selector) === false ) { return false; }
+ }
+ return safe.testPattern(matcher, `${a}`);
+ };
+ Object.defineProperty(owner, property, {
+ get: function() {
+ return current.get
+ ? current.get.call(this)
+ : current.value;
+ },
+ set: function(a) {
+ if ( shouldPreventSet(this, a) ) {
+ safe.uboLog(logPrefix, 'Assignment prevented');
+ } else if ( current.set ) {
+ current.set.call(this, a);
+ }
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Assigned:\n${a}`);
+ }
+ current.value = a;
+ },
+ });
+}
+registerScriptlet(freezeElementProperty, {
+ name: 'freeze-element-property.js',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/**
+ * @scriptlet prevent-innerHTML
+ *
+ * @description
+ * Conditionally prevent assignment to `innerHTML` property.
+ *
+ * @param [selector]
+ * Optional. The element must match `selector` for the prevention to take
+ * place.
+ *
+ * @param [pattern]
+ * Optional. A pattern to match against the assigned value. The pattern can be
+ * a plain string, or a regex. Prepend with `!` to reverse the match condition.
+ *
+ * */
+
+function preventInnerHTML(
+ selector = '',
+ pattern = ''
+) {
+ freezeElementProperty('innerHTML', selector, pattern);
+}
+registerScriptlet(preventInnerHTML, {
+ name: 'prevent-innerHTML.js',
+ dependencies: [
+ freezeElementProperty,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-navigation.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-navigation.js
new file mode 100644
index 00000000..81a4b579
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-navigation.js
@@ -0,0 +1,67 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2026-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/**
+ * @scriptlet prevent-navigation
+ *
+ * @description
+ * Conditionally abort navigation events.
+ *
+ * @param [pattern]
+ * Optional. A pattern to match against the assigned value. The pattern can be
+ * a plain string, or a regex. Prepend with `!` to reverse the match condition.
+ * No pattern
+ *
+ * Reference:
+ * https://github.com/AdguardTeam/Scriptlets/commit/cd2d8eefd5
+ * */
+
+export function preventNavigation(
+ pattern = ''
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('prevent-navigation', pattern);
+ const needle = pattern === 'location.href' ? self.location.href : pattern;
+ const matcher = safe.initPattern(needle, { canNegate: true });
+ self.navigation.addEventListener('navigate', ev => {
+ if ( ev.userInitiated ) { return; }
+ const { url } = ev.destination;
+ if ( pattern === '' ) {
+ safe.uboLog(logPrefix, `Navigation to ${url}`);
+ return;
+ }
+ if ( safe.testPattern(matcher, url) ) {
+ ev.preventDefault();
+ safe.uboLog(logPrefix, `Prevented navigation to ${url}`);
+ }
+ });
+}
+registerScriptlet(preventNavigation, {
+ name: 'prevent-navigation.js',
+ dependencies: [
+ safeSelf,
+ ],
+ world: 'ISOLATED',
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-settimeout.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-settimeout.js
new file mode 100644
index 00000000..289c4465
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-settimeout.js
@@ -0,0 +1,236 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+class RangeParser {
+ constructor(s) {
+ this.not = s.charAt(0) === '!';
+ if ( this.not ) { s = s.slice(1); }
+ if ( s === '' ) { return; }
+ const pos = s.indexOf('-');
+ if ( pos !== 0 ) {
+ this.min = this.max = parseInt(s, 10) || 0;
+ }
+ if ( pos !== -1 ) {
+ this.max = parseInt(s.slice(pos + 1), 10) || Number.MAX_SAFE_INTEGER;
+ }
+ }
+ unbound() {
+ return this.min === undefined && this.max === undefined;
+ }
+ test(v) {
+ const n = Math.min(Math.max(Number(v) || 0, 0), Number.MAX_SAFE_INTEGER);
+ if ( this.min === this.max ) {
+ return (this.min === undefined || n === this.min) !== this.not;
+ }
+ if ( this.min === undefined ) {
+ return (n <= this.max) !== this.not;
+ }
+ if ( this.max === undefined ) {
+ return (n >= this.min) !== this.not;
+ }
+ return (n >= this.min && n <= this.max) !== this.not;
+ }
+}
+registerScriptlet(RangeParser, {
+ name: 'range-parser.fn',
+});
+
+/**
+ * @scriptlet prevent-setTimeout
+ *
+ * @description
+ * Conditionally prevent execution of the callback function passed to native
+ * setTimeout method. With no parameters, all calls to setTimeout will be
+ * shown in the logger.
+ *
+ * @param [needle]
+ * A pattern to match against the stringified callback. The pattern can be a
+ * plain string, or a regex. Prepend with `!` to reverse the match condition.
+ *
+ * @param [delay]
+ * A value to match against the delay. Can be a single value for exact match,
+ * or a range:
+ * - `min-max`: matches if delay >= min and delay <= max
+ * - `min-`: matches if delay >= min
+ * - `-max`: matches if delay <= max
+ * No delay means to match any delay value.
+ * Prepend with `!` to reverse the match condition.
+ *
+ * */
+
+export function preventSetTimeout(
+ needleRaw = '',
+ delayRaw = ''
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('prevent-setTimeout', needleRaw, delayRaw);
+ const needleNot = needleRaw.charAt(0) === '!';
+ const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw);
+ const range = new RangeParser(delayRaw);
+ proxyApplyFn('setTimeout', function(context) {
+ const { callArgs } = context;
+ const a = callArgs[0] instanceof Function
+ ? safe.String(safe.Function_toString(callArgs[0]))
+ : safe.String(callArgs[0]);
+ const b = callArgs[1];
+ if ( needleRaw === '' && range.unbound() ) {
+ safe.uboLog(logPrefix, `Called:\n${a}\n${b}`);
+ return context.reflect();
+ }
+ if ( reNeedle.test(a) !== needleNot && range.test(b) ) {
+ callArgs[0] = function(){};
+ safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`);
+ }
+ return context.reflect();
+ });
+}
+registerScriptlet(preventSetTimeout, {
+ name: 'prevent-setTimeout.js',
+ aliases: [
+ 'no-setTimeout-if.js',
+ 'nostif.js',
+ 'setTimeout-defuser.js',
+ ],
+ dependencies: [
+ proxyApplyFn,
+ RangeParser,
+ safeSelf,
+ ],
+});
+
+/**
+ * @scriptlet prevent-setInterval
+ *
+ * @description
+ * Conditionally prevent execution of the callback function passed to native
+ * setInterval method. With no parameters, all calls to setInterval will be
+ * shown in the logger.
+ *
+ * @param [needle]
+ * A pattern to match against the stringified callback. The pattern can be a
+ * plain string, or a regex. Prepend with `!` to reverse the match condition.
+ * No pattern means to match anything.
+ *
+ * @param [delay]
+ * A value to match against the delay. Can be a single value for exact match,
+ * or a range:
+ * - `min-max`: matches if delay >= min and delay <= max
+ * - `min-`: matches if delay >= min
+ * - `-max`: matches if delay <= max
+ * No delay means to match any delay value.
+ * Prepend with `!` to reverse the match condition.
+ *
+ * */
+
+export function preventSetInterval(
+ needleRaw = '',
+ delayRaw = ''
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('prevent-setInterval', needleRaw, delayRaw);
+ const needleNot = needleRaw.charAt(0) === '!';
+ const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw);
+ const range = new RangeParser(delayRaw);
+ proxyApplyFn('setInterval', function(context) {
+ const { callArgs } = context;
+ const a = callArgs[0] instanceof Function
+ ? safe.String(safe.Function_toString(callArgs[0]))
+ : safe.String(callArgs[0]);
+ const b = callArgs[1];
+ if ( needleRaw === '' && range.unbound() ) {
+ safe.uboLog(logPrefix, `Called:\n${a}\n${b}`);
+ return context.reflect();
+ }
+ if ( reNeedle.test(a) !== needleNot && range.test(b) ) {
+ callArgs[0] = function(){};
+ safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`);
+ }
+ return context.reflect();
+ });
+}
+registerScriptlet(preventSetInterval, {
+ name: 'prevent-setInterval.js',
+ aliases: [
+ 'no-setInterval-if.js',
+ 'nosiif.js',
+ 'setInterval-defuser.js',
+ ],
+ dependencies: [
+ proxyApplyFn,
+ RangeParser,
+ safeSelf,
+ ],
+});
+
+/**
+ * @scriptlet prevent-requestAnimationFrame
+ *
+ * @description
+ * Conditionally prevent execution of the callback function passed to native
+ * requestAnimationFrame method. With no parameters, all calls to
+ * requestAnimationFrame will be shown in the logger.
+ *
+ * @param [needle]
+ * A pattern to match against the stringified callback. The pattern can be a
+ * plain string, or a regex.
+ * Prepend with `!` to reverse the match condition.
+ *
+ * */
+
+export function preventRequestAnimationFrame(
+ needleRaw = ''
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('prevent-requestAnimationFrame', needleRaw);
+ const needleNot = needleRaw.charAt(0) === '!';
+ const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw);
+ proxyApplyFn('requestAnimationFrame', function(context) {
+ const { callArgs } = context;
+ const a = callArgs[0] instanceof Function
+ ? safe.String(safe.Function_toString(callArgs[0]))
+ : safe.String(callArgs[0]);
+ if ( needleRaw === '' ) {
+ safe.uboLog(logPrefix, `Called:\n${a}`);
+ } else if ( reNeedle.test(a) !== needleNot ) {
+ callArgs[0] = function(){};
+ safe.uboLog(logPrefix, `Prevented:\n${a}`);
+ }
+ return context.reflect();
+ });
+}
+registerScriptlet(preventRequestAnimationFrame, {
+ name: 'prevent-requestAnimationFrame.js',
+ aliases: [
+ 'no-requestAnimationFrame-if.js',
+ 'norafif.js',
+ ],
+ dependencies: [
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-xhr.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-xhr.js
new file mode 100644
index 00000000..47847087
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/prevent-xhr.js
@@ -0,0 +1,329 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import {
+ generateContentFn,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+} from './utils.js';
+import {
+ proxyApplyFn,
+ proxyToStringFn,
+} from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+// Externally added to the private namespace in which scriptlets execute.
+/* global scriptletGlobals */
+
+/******************************************************************************/
+
+export function modifyXhrResponseFn(
+ propsToMatch = '',
+ modifierFn = ''
+) {
+ if ( typeof propsToMatch !== 'string' ) { return; }
+ const safe = safeSelf();
+ if ( modifyXhrResponseFn.xhrInstances === undefined ) {
+ modifyXhrResponseFn.xhrInstances = new WeakMap();
+ }
+ const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
+ const NativeXMLHttpRequest = self.XMLHttpRequest;
+ const TrappedXMLHttpRequest = class XMLHttpRequest extends NativeXMLHttpRequest {
+ open(method, url, ...args) {
+ const haystack = { method, url };
+ if ( propsToMatch === '' ) {
+ safe.uboLog(`modifyXhrResponseFn() / Called: ${safe.JSON_stringify(haystack, null, 2)}`);
+ } else if ( matchObjectPropertiesFn(propNeedles, haystack) ) {
+ modifyXhrResponseFn.xhrInstances.set(this, modifierFn);
+ }
+ return super.open(method, url, ...args);
+ }
+ get response() {
+ const modifierFn = modifyXhrResponseFn.xhrInstances.get(this);
+ return modifierFn
+ ? modifierFn(this, super.response)
+ : super.response;
+ }
+ get responseText() {
+ const modifierFn = modifyXhrResponseFn.xhrInstances.get(this);
+ return modifierFn
+ ? modifierFn(this, super.responseText)
+ : super.responseText;
+ }
+ get responseXML() {
+ const modifierFn = modifyXhrResponseFn.xhrInstances.get(this);
+ return modifierFn
+ ? modifierFn(this, super.responseXML)
+ : super.responseXML;
+ }
+ };
+ proxyToStringFn(TrappedXMLHttpRequest.prototype.open, NativeXMLHttpRequest.prototype.open);
+ proxyToStringFn(TrappedXMLHttpRequest, NativeXMLHttpRequest);
+ self.XMLHttpRequest = TrappedXMLHttpRequest;
+}
+registerScriptlet(modifyXhrResponseFn, {
+ name: 'modify-xhr-response.fn',
+ dependencies: [
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ proxyToStringFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+function preventXhrFn(
+ trusted = false,
+ propsToMatch = '',
+ directive = ''
+) {
+ if ( typeof propsToMatch !== 'string' ) { return; }
+ const safe = safeSelf();
+ const scriptletName = trusted ? 'trusted-prevent-xhr' : 'prevent-xhr';
+ const logPrefix = safe.makeLogPrefix(scriptletName, propsToMatch, directive);
+ const xhrInstances = new WeakMap();
+ const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
+ const warOrigin = scriptletGlobals.warOrigin;
+ const safeDispatchEvent = (xhr, type) => {
+ try {
+ xhr.dispatchEvent(new Event(type));
+ } catch {
+ }
+ };
+ proxyApplyFn('XMLHttpRequest.prototype.open', function(context) {
+ const { thisArg, callArgs } = context;
+ xhrInstances.delete(thisArg);
+ const [ method, url, ...args ] = callArgs;
+ if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
+ return context.reflect();
+ }
+ const haystack = { method, url };
+ if ( propsToMatch === '' && directive === '' ) {
+ safe.uboLog(logPrefix, `Called: ${safe.JSON_stringify(haystack, null, 2)}`);
+ return context.reflect();
+ }
+ if ( matchObjectPropertiesFn(propNeedles, haystack) ) {
+ const xhrDetails = Object.assign(haystack, {
+ xhr: thisArg,
+ defer: args.length === 0 || !!args[0],
+ directive,
+ headers: {
+ 'date': '',
+ 'content-type': '',
+ 'content-length': '',
+ },
+ url: haystack.url,
+ props: {
+ response: { value: '' },
+ responseText: { value: '' },
+ responseXML: { value: null },
+ },
+ });
+ xhrInstances.set(thisArg, xhrDetails);
+ }
+ return context.reflect();
+ });
+ proxyApplyFn('XMLHttpRequest.prototype.send', function(context) {
+ const { thisArg } = context;
+ const xhrDetails = xhrInstances.get(thisArg);
+ if ( xhrDetails === undefined ) {
+ return context.reflect();
+ }
+ xhrDetails.headers['date'] = (new Date()).toUTCString();
+ let xhrText = '';
+ switch ( thisArg.responseType ) {
+ case 'arraybuffer':
+ xhrDetails.props.response.value = new ArrayBuffer(0);
+ xhrDetails.headers['content-type'] = 'application/octet-stream';
+ break;
+ case 'blob':
+ xhrDetails.props.response.value = new Blob([]);
+ xhrDetails.headers['content-type'] = 'application/octet-stream';
+ break;
+ case 'document': {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString('', 'text/html');
+ xhrDetails.props.response.value = doc;
+ xhrDetails.props.responseXML.value = doc;
+ xhrDetails.headers['content-type'] = 'text/html';
+ break;
+ }
+ case 'json':
+ xhrDetails.props.response.value = {};
+ xhrDetails.props.responseText.value = '{}';
+ xhrDetails.headers['content-type'] = 'application/json';
+ break;
+ default: {
+ if ( directive === '' ) { break; }
+ xhrText = generateContentFn(trusted, xhrDetails.directive);
+ if ( xhrText instanceof Promise ) {
+ xhrText = xhrText.then(text => {
+ xhrDetails.props.response.value = text;
+ xhrDetails.props.responseText.value = text;
+ });
+ } else {
+ xhrDetails.props.response.value = xhrText;
+ xhrDetails.props.responseText.value = xhrText;
+ }
+ xhrDetails.headers['content-type'] = 'text/plain';
+ break;
+ }
+ }
+ if ( xhrDetails.defer === false ) {
+ xhrDetails.headers['content-length'] = `${xhrDetails.props.response.value}`.length;
+ Object.defineProperties(xhrDetails.xhr, {
+ readyState: { value: 4 },
+ responseURL: { value: xhrDetails.url },
+ status: { value: 200 },
+ statusText: { value: 'OK' },
+ });
+ Object.defineProperties(xhrDetails.xhr, xhrDetails.props);
+ return;
+ }
+ Promise.resolve(xhrText).then(( ) => xhrDetails).then(details => {
+ Object.defineProperties(details.xhr, {
+ readyState: { value: 1, configurable: true },
+ responseURL: { value: xhrDetails.url },
+ });
+ safeDispatchEvent(details.xhr, 'readystatechange');
+ return details;
+ }).then(details => {
+ xhrDetails.headers['content-length'] = `${details.props.response.value}`.length;
+ Object.defineProperties(details.xhr, {
+ readyState: { value: 2, configurable: true },
+ status: { value: 200 },
+ statusText: { value: 'OK' },
+ });
+ safeDispatchEvent(details.xhr, 'readystatechange');
+ return details;
+ }).then(details => {
+ Object.defineProperties(details.xhr, {
+ readyState: { value: 3, configurable: true },
+ });
+ Object.defineProperties(details.xhr, details.props);
+ safeDispatchEvent(details.xhr, 'readystatechange');
+ return details;
+ }).then(details => {
+ Object.defineProperties(details.xhr, {
+ readyState: { value: 4 },
+ });
+ safeDispatchEvent(details.xhr, 'readystatechange');
+ safeDispatchEvent(details.xhr, 'load');
+ safeDispatchEvent(details.xhr, 'loadend');
+ safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`);
+ });
+ });
+ proxyApplyFn('XMLHttpRequest.prototype.getResponseHeader', function(context) {
+ const { thisArg } = context;
+ const xhrDetails = xhrInstances.get(thisArg);
+ if ( xhrDetails === undefined || thisArg.readyState < thisArg.HEADERS_RECEIVED ) {
+ return context.reflect();
+ }
+ const headerName = `${context.callArgs[0]}`;
+ const value = xhrDetails.headers[headerName.toLowerCase()];
+ if ( value !== undefined && value !== '' ) { return value; }
+ return null;
+ });
+ proxyApplyFn('XMLHttpRequest.prototype.getAllResponseHeaders', function(context) {
+ const { thisArg } = context;
+ const xhrDetails = xhrInstances.get(thisArg);
+ if ( xhrDetails === undefined || thisArg.readyState < thisArg.HEADERS_RECEIVED ) {
+ return context.reflect();
+ }
+ const out = [];
+ for ( const [ name, value ] of Object.entries(xhrDetails.headers) ) {
+ if ( !value ) { continue; }
+ out.push(`${name}: ${value}`);
+ }
+ if ( out.length !== 0 ) { out.push(''); }
+ return out.join('\r\n');
+ });
+}
+registerScriptlet(preventXhrFn, {
+ name: 'prevent-xhr.fn',
+ dependencies: [
+ generateContentFn,
+ matchObjectPropertiesFn,
+ parsePropertiesToMatchFn,
+ proxyApplyFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet prevent-xhr
+ *
+ * @description
+ * Prevent a XMLHttpRequest-baesed request from being sent to a remote server.
+ *
+ * @param propsToMatch
+ * The fetch arguments to match for the prevention to be triggered. The
+ * untrusted flavor limits the realm of response to return to safe values.
+ *
+ * @param [responseBody]
+ * Optional. The reponse to return when the prevention occurs. The response
+ * must be a safe constant value.
+ *
+ * */
+
+function preventXhr(...args) {
+ preventXhrFn(false, ...args);
+}
+registerScriptlet(preventXhr, {
+ name: 'prevent-xhr.js',
+ aliases: [
+ 'no-xhr-if.js',
+ ],
+ dependencies: [
+ preventXhrFn,
+ ],
+});
+
+/******************************************************************************/
+/**
+ * @scriptlet trusted-prevent-xhr
+ *
+ * @description
+ * Prevent a XMLHttpRequest-based request from being sent to a remote server.
+ *
+ * @param propsToMatch
+ * The fetch arguments to match for the prevention to be triggered. The
+ * untrusted flavor limits the realm of response to return to safe values.
+ *
+ * @param [responseBody]
+ * Optional. The reponse to return when the prevention occurs. The trusted
+ * version allows arbitrary response.
+ *
+ * */
+
+function trustedPreventXhr(...args) {
+ preventXhrFn(true, ...args);
+}
+registerScriptlet(trustedPreventXhr, {
+ name: 'trusted-prevent-xhr.js',
+ dependencies: [
+ preventXhrFn,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/proxy-apply.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/proxy-apply.js
new file mode 100644
index 00000000..9f32a643
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/proxy-apply.js
@@ -0,0 +1,169 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+
+/******************************************************************************/
+
+export function proxyToStringFn(proxiedFn, nativeFn) {
+ if ( proxyToStringFn.proxies === undefined ) {
+ proxyToStringFn.proxies = new WeakMap();
+ proxyToStringFn.nativeToString = Function.prototype.toString;
+ const proxiedToString = new Proxy(Function.prototype.toString, {
+ apply(target, thisArg) {
+ let proxied = thisArg;
+ for(;;) {
+ const fn = proxyToStringFn.proxies.get(proxied);
+ if ( fn === undefined ) { break; }
+ proxied = fn;
+ }
+ return proxyToStringFn.nativeToString.call(proxied);
+ }
+ });
+ proxyToStringFn.proxies.set(proxiedToString, proxyToStringFn.nativeToString);
+ Function.prototype.toString = proxiedToString;
+ }
+ proxyToStringFn.proxies.set(proxiedFn, nativeFn);
+}
+registerScriptlet(proxyToStringFn, {
+ name: 'proxy-tostring.fn',
+});
+
+/******************************************************************************/
+
+export function proxyApplyFn(
+ target = '',
+ handler = '',
+ options = {}
+) {
+ let context = globalThis;
+ let prop = target;
+ for (;;) {
+ const pos = prop.indexOf('.');
+ if ( pos === -1 ) { break; }
+ context = context[prop.slice(0, pos)];
+ if ( context instanceof Object === false ) { return; }
+ prop = prop.slice(pos+1);
+ }
+ const fn = context[prop];
+ if ( typeof fn !== 'function' ) { return; }
+ if ( proxyApplyFn.CtorContext === undefined ) {
+ proxyApplyFn.ctorContexts = [];
+ proxyApplyFn.CtorContext = class {
+ constructor(...args) {
+ this.init(...args);
+ }
+ init(callFn, callArgs) {
+ this.callFn = callFn;
+ this.callArgs = callArgs;
+ return this;
+ }
+ reflect() {
+ const r = Reflect.construct(this.callFn, this.callArgs);
+ this.callFn = this.callArgs = this.private = undefined;
+ proxyApplyFn.ctorContexts.push(this);
+ return r;
+ }
+ static factory(...args) {
+ return proxyApplyFn.ctorContexts.length !== 0
+ ? proxyApplyFn.ctorContexts.pop().init(...args)
+ : new proxyApplyFn.CtorContext(...args);
+ }
+ };
+ proxyApplyFn.applyContexts = [];
+ proxyApplyFn.ApplyContext = class {
+ constructor(...args) {
+ this.init(...args);
+ }
+ init(callFn, thisArg, callArgs) {
+ this.callFn = callFn;
+ this.thisArg = thisArg;
+ this.callArgs = callArgs;
+ return this;
+ }
+ reflect() {
+ const r = Reflect.apply(this.callFn, this.thisArg, this.callArgs);
+ this.callFn = this.thisArg = this.callArgs = this.private = undefined;
+ proxyApplyFn.applyContexts.push(this);
+ return r;
+ }
+ static factory(...args) {
+ return proxyApplyFn.applyContexts.length !== 0
+ ? proxyApplyFn.applyContexts.pop().init(...args)
+ : new proxyApplyFn.ApplyContext(...args);
+ }
+ };
+ proxyApplyFn.isCtor = new Map();
+ proxyApplyFn.proxies = new WeakMap();
+ if ( (options.skipToString || proxyApplyFn.skipToString) !== true ) {
+ proxyApplyFn.nativeToString = Function.prototype.toString;
+ const proxiedToString = new Proxy(Function.prototype.toString, {
+ apply(target, thisArg) {
+ let proxied = thisArg;
+ for(;;) {
+ const fn = proxyApplyFn.proxies.get(proxied);
+ if ( fn === undefined ) { break; }
+ proxied = fn;
+ }
+ return proxyApplyFn.nativeToString.call(proxied);
+ }
+ });
+ proxyApplyFn.proxies.set(proxiedToString, proxyApplyFn.nativeToString);
+ Function.prototype.toString = proxiedToString;
+ }
+ }
+ if ( proxyApplyFn.isCtor.has(target) === false ) {
+ proxyApplyFn.isCtor.set(target, fn.prototype?.constructor === fn);
+ }
+ const proxyDetails = {
+ apply(target, thisArg, args) {
+ return handler(proxyApplyFn.ApplyContext.factory(target, thisArg, args));
+ }
+ };
+ if ( proxyApplyFn.isCtor.get(target) ) {
+ proxyDetails.construct = function(target, args) {
+ return handler(proxyApplyFn.CtorContext.factory(target, args));
+ };
+ }
+ const proxiedTarget = new Proxy(fn, proxyDetails);
+ proxyApplyFn.proxies.set(proxiedTarget, fn);
+ context[prop] = proxiedTarget;
+}
+registerScriptlet(proxyApplyFn, {
+ name: 'proxy-apply.fn',
+});
+
+/******************************************************************************/
+
+export function proxyApplyConfig(config = '') {
+ try {
+ if ( typeof proxyApplyFn !== 'function' ) { return; }
+ config = JSON.parse(config);
+ if ( typeof config !== 'object' ) { return; }
+ Object.assign(proxyApplyFn, config);
+ } catch {
+ }
+}
+registerScriptlet(proxyApplyConfig , {
+ name: 'proxy-apply-config.js',
+ priority: 100,
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/replace-argument.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/replace-argument.js
new file mode 100644
index 00000000..0c98217a
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/replace-argument.js
@@ -0,0 +1,133 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { parseReplaceFn } from './parse-replace.js';
+import { proxyApplyFn } from './proxy-apply.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+import { validateConstantFn } from './set-constant.js';
+
+/**
+ * @scriptlet trusted-replace-argument.js
+ *
+ * @description
+ * Replace an argument passed to a method. Requires a trusted source.
+ *
+ * @param propChain
+ * The property chain to the function which argument must be replaced when
+ * called.
+ *
+ * @param argposRaw
+ * The zero-based position of the argument in the argument list. Use a negative
+ * number for a position relative to the last argument. Use literal `this` to
+ * replace the value used in `prototype`-based methods.
+ *
+ * @param argraw
+ * The replacement value, validated using the same heuristic as with the
+ * `set-constant.js` scriptlet.
+ * If the replacement value matches `json:...`, the value will be the
+ * json-parsed string after `json:`.
+ * If the replacement value matches `repl:/.../.../`, the target argument will
+ * be replaced according the regex-replacement directive following `repl:`
+ * If the replacement value matches `add:number`, number will be added to the
+ * target argument.
+ *
+ * @param [, condition, pattern]
+ * Optional. The replacement will occur only when pattern matches the target
+ * argument.
+ *
+ * */
+
+export function trustedReplaceArgument(
+ propChain = '',
+ argposRaw = '',
+ argraw = '',
+ ...varargs
+) {
+ if ( propChain === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('trusted-replace-argument', propChain, argposRaw, argraw);
+ const argoffset = parseInt(argposRaw, 10) || 0;
+ const extraArgs = safe.parseVarargs(varargs);
+ let replacer;
+ if ( argraw.startsWith('repl:/') ) {
+ const parsed = parseReplaceFn(argraw.slice(5));
+ if ( parsed === undefined ) { return; }
+ replacer = arg => `${arg}`.replace(replacer.re, replacer.replacement);
+ Object.assign(replacer, parsed);
+ } else if ( argraw.startsWith('add:') ) {
+ const delta = parseFloat(argraw.slice(4));
+ if ( isNaN(delta) ) { return; }
+ replacer = arg => Number(arg) + delta;
+ } else {
+ const value = validateConstantFn(true, argraw, extraArgs);
+ replacer = ( ) => value;
+ }
+ const reCondition = extraArgs.condition
+ ? safe.patternToRegex(`${extraArgs.condition}`)
+ : /^/;
+ const getArg = context => {
+ if ( argposRaw === 'this' ) { return context.thisArg; }
+ const { callArgs } = context;
+ const argpos = argoffset >= 0 ? argoffset : callArgs.length - argoffset;
+ if ( argpos < 0 || argpos >= callArgs.length ) { return; }
+ context.private = { argpos };
+ return callArgs[argpos];
+ };
+ const setArg = (context, value) => {
+ if ( argposRaw === 'this' ) {
+ if ( value !== context.thisArg ) {
+ context.thisArg = value;
+ }
+ } else if ( context.private ) {
+ context.callArgs[context.private.argpos] = value;
+ }
+ };
+ proxyApplyFn(propChain, function(context) {
+ if ( argposRaw === '' ) {
+ safe.uboLog(logPrefix, `Arguments:\n${context.callArgs.join('\n')}`);
+ return context.reflect();
+ }
+ const argBefore = getArg(context);
+ if ( extraArgs.condition !== undefined ) {
+ if ( safe.RegExp_test(reCondition, argBefore) === false ) {
+ return context.reflect();
+ }
+ }
+ const argAfter = replacer(argBefore);
+ if ( argAfter !== argBefore ) {
+ setArg(context, argAfter);
+ safe.uboLog(logPrefix, `Replaced argument:\nBefore: ${JSON.stringify(argBefore)}\nAfter: ${argAfter}`);
+ }
+ return context.reflect();
+ });
+}
+registerScriptlet(trustedReplaceArgument, {
+ name: 'trusted-replace-argument.js',
+ requiresTrust: true,
+ dependencies: [
+ parseReplaceFn,
+ proxyApplyFn,
+ safeSelf,
+ validateConstantFn,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/run-at.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/run-at.js
new file mode 100644
index 00000000..35036d2b
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/run-at.js
@@ -0,0 +1,96 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/* eslint no-prototype-builtins: 0 */
+
+/**
+ * @helperScriptlet run-at.fn
+ *
+ * @description
+ * Execute a function at a specific page-load milestone.
+ *
+ * @param fn
+ * The function to call.
+ *
+ * @param when
+ * An identifier which tells when the function should be executed.
+ * See .
+ *
+ * @example
+ * `runAt(( ) => { start(); }, 'interactive')`
+ *
+ * */
+
+export function runAt(fn, when) {
+ const intFromReadyState = state => {
+ const targets = {
+ 'loading': 1, 'asap': 1,
+ 'interactive': 2, 'end': 2, '2': 2,
+ 'complete': 3, 'idle': 3, '3': 3,
+ };
+ const tokens = Array.isArray(state) ? state : [ state ];
+ for ( const token of tokens ) {
+ const prop = `${token}`;
+ if ( Object.hasOwn(targets, prop) === false ) { continue; }
+ return targets[prop];
+ }
+ return 0;
+ };
+ const runAt = intFromReadyState(when);
+ if ( intFromReadyState(document.readyState) >= runAt ) {
+ fn(); return;
+ }
+ const onStateChange = ( ) => {
+ if ( intFromReadyState(document.readyState) < runAt ) { return; }
+ fn();
+ safe.removeEventListener.apply(document, args);
+ };
+ const safe = safeSelf();
+ const args = [ 'readystatechange', onStateChange, { capture: true } ];
+ safe.addEventListener.apply(document, args);
+}
+registerScriptlet(runAt, {
+ name: 'run-at.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+export function runAtHtmlElementFn(fn) {
+ if ( document.documentElement ) {
+ fn();
+ return;
+ }
+ const observer = new MutationObserver(( ) => {
+ observer.disconnect();
+ fn();
+ });
+ observer.observe(document, { childList: true });
+}
+registerScriptlet(runAtHtmlElementFn, {
+ name: 'run-at-html-element.fn',
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/safe-self.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/safe-self.js
new file mode 100644
index 00000000..0e8dc3fd
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/safe-self.js
@@ -0,0 +1,210 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+
+/******************************************************************************/
+
+// Externally added to the private namespace in which scriptlets execute.
+/* global scriptletGlobals */
+
+export function safeSelf() {
+ if ( safeSelf.safe ) {
+ return safeSelf.safe;
+ }
+ const self = globalThis;
+ const safe = {
+ 'Array_from': Array.from,
+ 'Error': self.Error,
+ 'Function_toString': Function.prototype.call.bind(self.Function.prototype.toString),
+ 'Math_floor': Math.floor,
+ 'Math_max': Math.max,
+ 'Math_min': Math.min,
+ 'Math_random': Math.random,
+ 'Object': Object,
+ 'Object_defineProperty': Object.defineProperty.bind(Object),
+ 'Object_defineProperties': Object.defineProperties.bind(Object),
+ 'Object_fromEntries': Object.fromEntries.bind(Object),
+ 'Object_getOwnPropertyDescriptor': Object.getOwnPropertyDescriptor.bind(Object),
+ 'Object_hasOwn': Object.hasOwn.bind(Object),
+ 'Object_toString': Object.prototype.toString,
+ 'RegExp': self.RegExp,
+ 'RegExp_test': Function.prototype.call.bind(self.RegExp.prototype.test),
+ 'RegExp_exec': self.RegExp.prototype.exec,
+ 'Request_clone': self.Request.prototype.clone,
+ 'String': self.String,
+ 'String_fromCharCode': String.fromCharCode,
+ 'String_includes': String.prototype.includes,
+ 'String_split': String.prototype.split,
+ 'XMLHttpRequest': self.XMLHttpRequest,
+ 'XMLHttpRequest_open': Function.prototype.call.bind(self.XMLHttpRequest.prototype.open),
+ 'XMLHttpRequest_send': Function.prototype.call.bind(self.XMLHttpRequest.prototype.send),
+ 'addEventListener': self.EventTarget.prototype.addEventListener,
+ 'removeEventListener': self.EventTarget.prototype.removeEventListener,
+ 'fetch': self.fetch,
+ 'JSON': self.JSON,
+ 'JSON_parse': Function.prototype.call.bind(self.JSON.parse, self.JSON),
+ 'JSON_stringify': Function.prototype.call.bind(self.JSON.stringify, self.JSON),
+ 'structuredClone': Function.prototype.call.bind(self.structuredClone, self),
+ 'log': console.log.bind(console),
+ // Properties
+ logLevel: 0,
+ // Methods
+ makeLogPrefix(...args) {
+ return this.sendToLogger && `[${args.join(' \u205D ')}]` || '';
+ },
+ uboLog(...args) {
+ if ( this.sendToLogger === undefined ) { return; }
+ if ( args === undefined || args[0] === '' ) { return; }
+ return this.sendToLogger('info', ...args);
+
+ },
+ uboErr(...args) {
+ if ( this.sendToLogger === undefined ) { return; }
+ if ( args === undefined || args[0] === '' ) { return; }
+ return this.sendToLogger('error', ...args);
+ },
+ escapeRegexChars(s) {
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ },
+ initPattern(pattern, options = {}) {
+ if ( pattern === '' ) {
+ return { matchAll: true, expect: true };
+ }
+ const expect = (options.canNegate !== true || pattern.startsWith('!') === false);
+ if ( expect === false ) {
+ pattern = pattern.slice(1);
+ }
+ const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
+ if ( match !== null ) {
+ return {
+ re: new this.RegExp(
+ match[1],
+ match[2] || options.flags
+ ),
+ expect,
+ };
+ }
+ if ( options.flags !== undefined ) {
+ return {
+ re: new this.RegExp(this.escapeRegexChars(pattern),
+ options.flags
+ ),
+ expect,
+ };
+ }
+ return { pattern, expect };
+ },
+ testPattern(details, haystack) {
+ if ( details.matchAll ) { return true; }
+ if ( details.re ) {
+ return this.RegExp_test(details.re, haystack) === details.expect;
+ }
+ return this.String_includes.call(`${haystack}`, details.pattern) === details.expect;
+ },
+ patternToRegex(pattern, flags = undefined, verbatim = false) {
+ if ( pattern === '' ) { return /^/; }
+ const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
+ if ( match === null ) {
+ const reStr = this.escapeRegexChars(pattern);
+ return new RegExp(verbatim ? `^${reStr}$` : reStr, flags);
+ }
+ try {
+ return new RegExp(match[1], match[2] || undefined);
+ }
+ catch {
+ }
+ return /^/;
+ },
+ parseVarargs(varargs) {
+ const entries = varargs.reduce((out, v, i, a) => {
+ if ( i & 1 ) { return out; }
+ const rawValue = a[i+1];
+ const value = /^\d+$/.test(rawValue)
+ ? parseInt(rawValue, 10)
+ : rawValue;
+ out.push([ a[i], value ]);
+ return out;
+ }, []);
+ return this.Object_fromEntries(entries);
+ },
+ };
+ safeSelf.safe = safe;
+ if ( scriptletGlobals.bcSecret === undefined ) { return safe; }
+ // This is executed only when the logger is opened
+ safe.logLevel = scriptletGlobals.logLevel || 1;
+ let lastLogType = '';
+ let lastLogText = '';
+ let lastLogTime = 0;
+ safe.toLogText = (type, ...args) => {
+ if ( args.length === 0 ) { return; }
+ const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`;
+ if ( text === lastLogText && type === lastLogType ) {
+ if ( (Date.now() - lastLogTime) < 5000 ) { return; }
+ }
+ lastLogType = type;
+ lastLogText = text;
+ lastLogTime = Date.now();
+ return text;
+ };
+ try {
+ const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret);
+ let bcBuffer = [];
+ safe.sendToLogger = (type, ...args) => {
+ const text = safe.toLogText(type, ...args);
+ if ( text === undefined ) { return; }
+ if ( bcBuffer === undefined ) {
+ return bc.postMessage({ what: 'messageToLogger', type, text });
+ }
+ bcBuffer.push({ type, text });
+ };
+ bc.onmessage = ev => {
+ const msg = ev.data;
+ switch ( msg ) {
+ case 'iamready!':
+ if ( bcBuffer === undefined ) { break; }
+ bcBuffer.forEach(({ type, text }) =>
+ bc.postMessage({ what: 'messageToLogger', type, text })
+ );
+ bcBuffer = undefined;
+ break;
+ case 'setScriptletLogLevelToOne':
+ safe.logLevel = 1;
+ break;
+ case 'setScriptletLogLevelToTwo':
+ safe.logLevel = 2;
+ break;
+ }
+ };
+ bc.postMessage('areyouready?');
+ } catch {
+ safe.sendToLogger = (type, ...args) => {
+ const text = safe.toLogText(type, ...args);
+ if ( text === undefined ) { return; }
+ safe.log(`uBO ${text}`);
+ };
+ }
+ return safe;
+}
+registerScriptlet(safeSelf, {
+ name: 'safe-self.fn',
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/scriptlets.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/scriptlets.js
new file mode 100755
index 00000000..cbb415a1
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/scriptlets.js
@@ -0,0 +1,2103 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import './abort-current-script.js';
+import './attribute.js';
+import './create-html.js';
+import './href-sanitizer.js';
+import './json-edit.js';
+import './json-prune.js';
+import './noeval.js';
+import './object-prune.js';
+import './prevent-addeventlistener.js';
+import './prevent-bab.js';
+import './prevent-clipboard-write.js';
+import './prevent-dialog.js';
+import './prevent-fetch.js';
+import './prevent-innerHTML.js';
+import './prevent-navigation.js';
+import './prevent-settimeout.js';
+import './replace-argument.js';
+import './spoof-css.js';
+import './vod.js';
+
+import {
+ collateFetchArgumentsFn,
+ getExceptionTokenFn,
+ getRandomTokenFn,
+ lookupElementsFn,
+ matchObjectPropertiesFn,
+ onIdleFn,
+ parsePropertiesToMatchFn,
+} from './utils.js';
+import {
+ runAt,
+ runAtHtmlElementFn,
+} from './run-at.js';
+
+import { getAllCookiesFn } from './cookie.js';
+import { getAllLocalStorageFn } from './localstorage.js';
+import { matchesStackTraceFn } from './stack-trace.js';
+import { modifyXhrResponseFn } from './prevent-xhr.js';
+import { proxyApplyFn } from './proxy-apply.js';
+import { registeredScriptlets } from './base.js';
+import { safeSelf } from './safe-self.js';
+import { validateConstantFn } from './set-constant.js';
+
+/* eslint no-prototype-builtins: 0 */
+
+export const builtinScriptlets = registeredScriptlets;
+
+/*******************************************************************************
+
+ Helper functions
+
+ These are meant to be used as dependencies to injectable scriptlets.
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'replace-node-text.fn',
+ fn: replaceNodeTextFn,
+ dependencies: [
+ 'get-random-token.fn',
+ 'run-at.fn',
+ 'safe-self.fn',
+ ],
+});
+function replaceNodeTextFn(
+ nodeName = '',
+ pattern = '',
+ replacement = '',
+ ...varargs
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('replace-node-text.fn', ...Array.from(arguments));
+ const reNodeName = safe.patternToRegex(nodeName, 'i', true);
+ const rePattern = safe.patternToRegex(pattern, 'gms');
+ const extraArgs = safe.parseVarargs(varargs);
+ const reIncludes = extraArgs.includes || extraArgs.condition
+ ? safe.patternToRegex(extraArgs.includes || extraArgs.condition, 'ms')
+ : null;
+ const reExcludes = extraArgs.excludes
+ ? safe.patternToRegex(extraArgs.excludes, 'ms')
+ : null;
+ const textContentFactory = (( ) => {
+ const out = { createScript: s => s };
+ const { trustedTypes: tt } = self;
+ if ( tt instanceof Object ) {
+ if ( typeof tt.getPropertyType === 'function' ) {
+ if ( tt.getPropertyType('script', 'textContent') === 'TrustedScript' ) {
+ return tt.createPolicy(getRandomTokenFn(), out);
+ }
+ }
+ }
+ return out;
+ })();
+ let sedCount = extraArgs.sedCount ?? Number.MAX_SAFE_INTEGER;
+ const handleNode = node => {
+ const before = node.textContent;
+ if ( reIncludes ) {
+ reIncludes.lastIndex = 0;
+ if ( safe.RegExp_test(reIncludes, before) === false ) { return; }
+ }
+ if ( reExcludes ) {
+ reExcludes.lastIndex = 0;
+ if ( safe.RegExp_test(reExcludes, before) ) { return; }
+ }
+ rePattern.lastIndex = 0;
+ if ( safe.RegExp_test(rePattern, before) === false ) { return; }
+ rePattern.lastIndex = 0;
+ const after = pattern !== ''
+ ? before.replace(rePattern, replacement)
+ : replacement;
+ node.textContent = node.nodeName === 'SCRIPT'
+ ? textContentFactory.createScript(after)
+ : after;
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Text before:\n${before.trim()}`);
+ }
+ safe.uboLog(logPrefix, `Text after:\n${after.trim()}`);
+ sedCount -= 1;
+ };
+ const handleTree = root => {
+ const treeWalker = document.createTreeWalker(root,
+ NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
+ );
+ const { currentScript } = document;
+ let count = 0;
+ for (;;) {
+ const node = treeWalker.nextNode();
+ if ( node === null ) { break; }
+ count += 1;
+ if ( node === currentScript ) { continue; }
+ if ( reNodeName.test(node.nodeName) ) {
+ handleNode(node);
+ } else if ( node.nodeName === 'TEMPLATE' ) {
+ count += handleTree(node.content);
+ } else {
+ continue;
+ }
+ if ( sedCount === 0 ) { break; }
+ }
+ return count;
+ };
+ if ( document.documentElement ) {
+ const count = handleTree(document.documentElement);
+ safe.uboLog(logPrefix, `${count} nodes present before installing mutation observer`);
+ }
+ const stay = Boolean(extraArgs.stay);
+ if ( sedCount === 0 && stay === false ) { return; }
+ const stop = (takeRecord = true) => {
+ const mutations = takeRecord ? observer.takeRecords() : [];
+ observer.disconnect();
+ handleMutations(mutations);
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, 'Quitting');
+ }
+ };
+ const handleMutations = mutations => {
+ for ( const mutation of mutations ) {
+ for ( const node of mutation.addedNodes ) {
+ if ( reNodeName.test(node.nodeName) ) {
+ handleNode(node);
+ } else if ( node.nodeName === 'TEMPLATE' ) {
+ handleTree(node.content);
+ } else {
+ continue;
+ }
+ if ( sedCount === 0 ) { return stop(false); }
+ }
+ }
+ };
+ const observer = new MutationObserver(handleMutations);
+ observer.observe(document, { childList: true, subtree: true });
+ if ( stay ) { return; }
+ runAt(( ) => {
+ const quitAfter = extraArgs.quitAfter ?? 0;
+ if ( quitAfter === 0 ) { return stop(); }
+ setTimeout(( ) => { stop(); }, quitAfter);
+ }, 'interactive');
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'replace-fetch-response.fn',
+ fn: replaceFetchResponseFn,
+ dependencies: [
+ 'collate-fetch-arguments.fn',
+ 'match-object-properties.fn',
+ 'parse-properties-to-match.fn',
+ 'safe-self.fn',
+ ],
+});
+function replaceFetchResponseFn(
+ trusted = false,
+ pattern = '',
+ replacement = '',
+ propsToMatch = '',
+ ...varargs
+) {
+ if ( trusted !== true ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('replace-fetch-response', pattern, replacement, propsToMatch);
+ if ( pattern === '*' ) { pattern = '.*'; }
+ const rePattern = safe.patternToRegex(pattern);
+ const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
+ const extraArgs = safe.parseVarargs(varargs);
+ const reIncludes = extraArgs.includes ? safe.patternToRegex(extraArgs.includes) : null;
+ self.fetch = new Proxy(self.fetch, {
+ apply: function(target, thisArg, args) {
+ const fetchPromise = Reflect.apply(target, thisArg, args);
+ if ( pattern === '' ) { return fetchPromise; }
+ if ( propNeedles.size !== 0 ) {
+ const props = collateFetchArgumentsFn(...args);
+ const matched = matchObjectPropertiesFn(propNeedles, props);
+ if ( matched === undefined ) { return fetchPromise; }
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Matched "propsToMatch":\n\t${matched.join('\n\t')}`);
+ }
+ }
+ return fetchPromise.then(responseBefore => {
+ const response = responseBefore.clone();
+ return response.text().then(textBefore => {
+ if ( reIncludes && reIncludes.test(textBefore) === false ) {
+ return responseBefore;
+ }
+ const textAfter = textBefore.replace(rePattern, replacement);
+ if ( textAfter === textBefore ) { return responseBefore; }
+ safe.uboLog(logPrefix, 'Replaced');
+ const responseAfter = new Response(textAfter, {
+ status: responseBefore.status,
+ statusText: responseBefore.statusText,
+ headers: responseBefore.headers,
+ });
+ Object.defineProperties(responseAfter, {
+ ok: { value: responseBefore.ok },
+ redirected: { value: responseBefore.redirected },
+ type: { value: responseBefore.type },
+ url: { value: responseBefore.url },
+ });
+ return responseAfter;
+ }).catch(reason => {
+ safe.uboErr(logPrefix, reason);
+ return responseBefore;
+ });
+ }).catch(reason => {
+ safe.uboErr(logPrefix, reason);
+ return fetchPromise;
+ });
+ }
+ });
+}
+
+
+
+/*******************************************************************************
+
+ Injectable scriptlets
+
+ These are meant to be used in the MAIN (webpage) execution world.
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'abort-on-property-read.js',
+ aliases: [
+ 'aopr.js',
+ ],
+ fn: abortOnPropertyRead,
+ dependencies: [
+ 'get-exception-token.fn',
+ 'safe-self.fn',
+ ],
+});
+function abortOnPropertyRead(
+ chain = ''
+) {
+ if ( typeof chain !== 'string' ) { return; }
+ if ( chain === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('abort-on-property-read', chain);
+ const exceptionToken = getExceptionTokenFn();
+ const abort = function() {
+ safe.uboLog(logPrefix, 'Aborted');
+ throw new ReferenceError(exceptionToken);
+ };
+ const makeProxy = function(owner, chain) {
+ const pos = chain.indexOf('.');
+ if ( pos === -1 ) {
+ const desc = Object.getOwnPropertyDescriptor(owner, chain);
+ if ( !desc || desc.get !== abort ) {
+ Object.defineProperty(owner, chain, {
+ get: abort,
+ set: function(){}
+ });
+ }
+ return;
+ }
+ const prop = chain.slice(0, pos);
+ let v = owner[prop];
+ chain = chain.slice(pos + 1);
+ if ( v ) {
+ makeProxy(v, chain);
+ return;
+ }
+ const desc = Object.getOwnPropertyDescriptor(owner, prop);
+ if ( desc && desc.set !== undefined ) { return; }
+ Object.defineProperty(owner, prop, {
+ get: function() { return v; },
+ set: function(a) {
+ v = a;
+ if ( a instanceof Object ) {
+ makeProxy(a, chain);
+ }
+ }
+ });
+ };
+ const owner = window;
+ makeProxy(owner, chain);
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'abort-on-property-write.js',
+ aliases: [
+ 'aopw.js',
+ ],
+ fn: abortOnPropertyWrite,
+ dependencies: [
+ 'get-exception-token.fn',
+ 'safe-self.fn',
+ ],
+});
+function abortOnPropertyWrite(
+ prop = ''
+) {
+ if ( typeof prop !== 'string' ) { return; }
+ if ( prop === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('abort-on-property-write', prop);
+ const exceptionToken = getExceptionTokenFn();
+ let owner = window;
+ for (;;) {
+ const pos = prop.indexOf('.');
+ if ( pos === -1 ) { break; }
+ owner = owner[prop.slice(0, pos)];
+ if ( owner instanceof Object === false ) { return; }
+ prop = prop.slice(pos + 1);
+ }
+ delete owner[prop];
+ Object.defineProperty(owner, prop, {
+ set: function() {
+ safe.uboLog(logPrefix, 'Aborted');
+ throw new ReferenceError(exceptionToken);
+ }
+ });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'adjust-setInterval.js',
+ aliases: [
+ 'nano-setInterval-booster.js',
+ 'nano-sib.js',
+ ],
+ fn: adjustSetInterval,
+ dependencies: [
+ 'safe-self.fn',
+ ],
+});
+// Imported from:
+// https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L126
+//
+// Speed up or down setInterval, 3 optional arguments.
+// The payload matcher, a string literal or a JavaScript RegExp, defaults
+// to match all.
+// delayMatcher
+// The delay matcher, an integer, defaults to 1000.
+// Use `*` to match any delay.
+// boostRatio - The delay multiplier when there is a match, 0.5 speeds up by
+// 2 times and 2 slows down by 2 times, defaults to 0.05 or speed up
+// 20 times. Speed up and down both cap at 50 times.
+function adjustSetInterval(
+ needleArg = '',
+ delayArg = '',
+ boostArg = ''
+) {
+ if ( typeof needleArg !== 'string' ) { return; }
+ const safe = safeSelf();
+ const reNeedle = safe.patternToRegex(needleArg);
+ let delay = delayArg !== '*' ? parseInt(delayArg, 10) : -1;
+ if ( isNaN(delay) || isFinite(delay) === false ) { delay = 1000; }
+ let boost = parseFloat(boostArg);
+ boost = isNaN(boost) === false && isFinite(boost)
+ ? Math.min(Math.max(boost, 0.001), 50)
+ : 0.05;
+ self.setInterval = new Proxy(self.setInterval, {
+ apply: function(target, thisArg, args) {
+ const [ a, b ] = args;
+ if (
+ (delay === -1 || b === delay) &&
+ reNeedle.test(a.toString())
+ ) {
+ args[1] = b * boost;
+ }
+ return target.apply(thisArg, args);
+ }
+ });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'adjust-setTimeout.js',
+ aliases: [
+ 'nano-setTimeout-booster.js',
+ 'nano-stb.js',
+ ],
+ fn: adjustSetTimeout,
+ dependencies: [
+ 'safe-self.fn',
+ ],
+});
+// Imported from:
+// https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L82
+//
+// Speed up or down setTimeout, 3 optional arguments.
+// funcMatcher
+// The payload matcher, a string literal or a JavaScript RegExp, defaults
+// to match all.
+// delayMatcher
+// The delay matcher, an integer, defaults to 1000.
+// Use `*` to match any delay.
+// boostRatio - The delay multiplier when there is a match, 0.5 speeds up by
+// 2 times and 2 slows down by 2 times, defaults to 0.05 or speed up
+// 20 times. Speed up and down both cap at 50 times.
+function adjustSetTimeout(
+ needleArg = '',
+ delayArg = '',
+ boostArg = ''
+) {
+ if ( typeof needleArg !== 'string' ) { return; }
+ const safe = safeSelf();
+ const reNeedle = safe.patternToRegex(needleArg);
+ let delay = delayArg !== '*' ? parseInt(delayArg, 10) : -1;
+ if ( isNaN(delay) || isFinite(delay) === false ) { delay = 1000; }
+ let boost = parseFloat(boostArg);
+ boost = isNaN(boost) === false && isFinite(boost)
+ ? Math.min(Math.max(boost, 0.001), 50)
+ : 0.05;
+ self.setTimeout = new Proxy(self.setTimeout, {
+ apply: function(target, thisArg, args) {
+ const [ a, b ] = args;
+ if (
+ (delay === -1 || b === delay) &&
+ reNeedle.test(a.toString())
+ ) {
+ args[1] = b * boost;
+ }
+ return target.apply(thisArg, args);
+ }
+ });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'prevent-refresh.js',
+ aliases: [
+ 'refresh-defuser.js',
+ ],
+ fn: preventRefresh,
+ world: 'ISOLATED',
+ dependencies: [
+ 'safe-self.fn',
+ ],
+});
+// https://www.reddit.com/r/uBlockOrigin/comments/q0frv0/while_reading_a_sports_article_i_was_redirected/hf7wo9v/
+function preventRefresh(
+ delay = ''
+) {
+ if ( typeof delay !== 'string' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('prevent-refresh', delay);
+ const stop = content => {
+ window.stop();
+ safe.uboLog(logPrefix, `Prevented "${content}"`);
+ };
+ const defuse = ( ) => {
+ const meta = document.querySelector('meta[http-equiv="refresh" i][content]');
+ if ( meta === null ) { return; }
+ const content = meta.getAttribute('content') || '';
+ const ms = delay === ''
+ ? Math.max(parseFloat(content) || 0, 0) * 500
+ : 0;
+ if ( ms === 0 ) {
+ stop(content);
+ } else {
+ setTimeout(( ) => { stop(content); }, ms);
+ }
+ };
+ self.addEventListener('load', defuse, { capture: true, once: true });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'remove-class.js',
+ aliases: [
+ 'rc.js',
+ ],
+ fn: removeClass,
+ world: 'ISOLATED',
+ dependencies: [
+ 'on-idle.fn',
+ 'run-at.fn',
+ 'safe-self.fn',
+ ],
+});
+function removeClass(
+ rawToken = '',
+ rawSelector = '',
+ behavior = ''
+) {
+ if ( typeof rawToken !== 'string' ) { return; }
+ if ( rawToken === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('remove-class', rawToken, rawSelector, behavior);
+ const tokens = safe.String_split.call(rawToken, /\s*\|\s*/);
+ const selector = tokens
+ .map(a => `${rawSelector}.${CSS.escape(a)}`)
+ .join(',');
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Target selector:\n\t${selector}`);
+ }
+ const mustStay = /\bstay\b/.test(behavior);
+ let timer;
+ const rmclass = ( ) => {
+ timer = undefined;
+ try {
+ const nodes = document.querySelectorAll(selector);
+ for ( const node of nodes ) {
+ node.classList.remove(...tokens);
+ safe.uboLog(logPrefix, 'Removed class(es)');
+ }
+ } catch {
+ }
+ if ( mustStay ) { return; }
+ if ( document.readyState !== 'complete' ) { return; }
+ observer.disconnect();
+ };
+ const mutationHandler = mutations => {
+ if ( timer !== undefined ) { return; }
+ let skip = true;
+ for ( let i = 0; i < mutations.length && skip; i++ ) {
+ const { type, addedNodes, removedNodes } = mutations[i];
+ if ( type === 'attributes' ) { skip = false; }
+ for ( let j = 0; j < addedNodes.length && skip; j++ ) {
+ if ( addedNodes[j].nodeType === 1 ) { skip = false; break; }
+ }
+ for ( let j = 0; j < removedNodes.length && skip; j++ ) {
+ if ( removedNodes[j].nodeType === 1 ) { skip = false; break; }
+ }
+ }
+ if ( skip ) { return; }
+ timer = onIdleFn(rmclass, { timeout: 67 });
+ };
+ const observer = new MutationObserver(mutationHandler);
+ const start = ( ) => {
+ rmclass();
+ observer.observe(document, {
+ attributes: true,
+ attributeFilter: [ 'class' ],
+ childList: true,
+ subtree: true,
+ });
+ };
+ runAt(( ) => {
+ start();
+ }, /\bcomplete\b/.test(behavior) ? 'idle' : 'loading');
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'webrtc-if.js',
+ fn: webrtcIf,
+ dependencies: [
+ 'safe-self.fn',
+ ],
+});
+function webrtcIf(
+ good = ''
+) {
+ if ( typeof good !== 'string' ) { return; }
+ const safe = safeSelf();
+ const reGood = safe.patternToRegex(good);
+ const rtcName = window.RTCPeerConnection
+ ? 'RTCPeerConnection'
+ : (window.webkitRTCPeerConnection ? 'webkitRTCPeerConnection' : '');
+ if ( rtcName === '' ) { return; }
+ const log = console.log.bind(console);
+ const neuteredPeerConnections = new WeakSet();
+ const isGoodConfig = function(instance, config) {
+ if ( neuteredPeerConnections.has(instance) ) { return false; }
+ if ( config instanceof Object === false ) { return true; }
+ if ( Array.isArray(config.iceServers) === false ) { return true; }
+ for ( const server of config.iceServers ) {
+ const urls = typeof server.urls === 'string'
+ ? [ server.urls ]
+ : server.urls;
+ if ( Array.isArray(urls) ) {
+ for ( const url of urls ) {
+ if ( reGood.test(url) ) { return true; }
+ }
+ }
+ if ( typeof server.username === 'string' ) {
+ if ( reGood.test(server.username) ) { return true; }
+ }
+ if ( typeof server.credential === 'string' ) {
+ if ( reGood.test(server.credential) ) { return true; }
+ }
+ }
+ neuteredPeerConnections.add(instance);
+ return false;
+ };
+ const peerConnectionCtor = window[rtcName];
+ const peerConnectionProto = peerConnectionCtor.prototype;
+ peerConnectionProto.createDataChannel =
+ new Proxy(peerConnectionProto.createDataChannel, {
+ apply: function(target, thisArg, args) {
+ if ( isGoodConfig(target, args[1]) === false ) {
+ log('uBO:', args[1]);
+ return Reflect.apply(target, thisArg, args.slice(0, 1));
+ }
+ return Reflect.apply(target, thisArg, args);
+ },
+ });
+ window[rtcName] =
+ new Proxy(peerConnectionCtor, {
+ construct: function(target, args) {
+ if ( isGoodConfig(target, args[0]) === false ) {
+ log('uBO:', args[0]);
+ return Reflect.construct(target);
+ }
+ return Reflect.construct(target, args);
+ }
+ });
+}
+
+/**
+ * @scriptlet prevent-window-open
+ *
+ * @description
+ * Prevent a webpage from opening new tabs through `window.open()`.
+ *
+ * @param pattern
+ * A plain string or regex to match against the `url` argument for the
+ * prevention to be triggered. If not provided, all calls to `window.open()`
+ * are prevented.
+ * If set to the special value `debug` *and* the logger is opened, the scriptlet
+ * will trigger a `debugger` statement and the prevention will not occur.
+ *
+ * @param [delay]
+ * If provided, a decoy will be created or opened, and this parameter states
+ * the number of seconds to wait for before the decoy is terminated, i.e.
+ * either removed from the DOM or closed.
+ *
+ * @param [decoy]
+ * A string representing the type of decoy to use:
+ * - `blank`: replace the `url` parameter with `about:blank`
+ * - `object`: create and append an `object` element to the DOM, and return
+ * its `contentWindow` property.
+ * - `frame`: create and append an `iframe` element to the DOM, and return
+ * its `contentWindow` property.
+ *
+ * @example
+ * ##+js(prevent-window-open, ads.example.com/)
+ *
+ * @example
+ * ##+js(prevent-window-open, ads.example.com/, 1, iframe)
+ *
+ * */
+
+builtinScriptlets.push({
+ name: 'prevent-window-open.js',
+ aliases: [
+ 'nowoif.js',
+ 'no-window-open-if.js',
+ 'window.open-defuser.js',
+ ],
+ fn: noWindowOpenIf,
+ dependencies: [
+ 'proxy-apply.fn',
+ 'safe-self.fn',
+ ],
+});
+function noWindowOpenIf(
+ pattern = '',
+ delay = '',
+ decoy = ''
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('no-window-open-if', pattern, delay, decoy);
+ const targetMatchResult = pattern.startsWith('!') === false;
+ if ( targetMatchResult === false ) {
+ pattern = pattern.slice(1);
+ }
+ const rePattern = safe.patternToRegex(pattern);
+ const autoRemoveAfter = (parseFloat(delay) || 0) * 1000;
+ const setTimeout = self.setTimeout;
+ const createDecoy = function(tag, urlProp, url) {
+ const decoyElem = document.createElement(tag);
+ decoyElem[urlProp] = url;
+ decoyElem.style.setProperty('height','1px', 'important');
+ decoyElem.style.setProperty('position','fixed', 'important');
+ decoyElem.style.setProperty('top','-1px', 'important');
+ decoyElem.style.setProperty('width','1px', 'important');
+ document.body.appendChild(decoyElem);
+ setTimeout(( ) => { decoyElem.remove(); }, autoRemoveAfter);
+ return decoyElem;
+ };
+ const noopFunc = function(){};
+ proxyApplyFn('open', function open(context) {
+ if ( pattern === 'debug' && safe.logLevel !== 0 ) {
+ debugger; // eslint-disable-line no-debugger
+ return context.reflect();
+ }
+ const { callArgs } = context;
+ const haystack = callArgs.join(' ');
+ if ( rePattern.test(haystack) !== targetMatchResult ) {
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Allowed (${callArgs.join(', ')})`);
+ }
+ return context.reflect();
+ }
+ safe.uboLog(logPrefix, `Prevented (${callArgs.join(', ')})`);
+ if ( delay === '' ) { return null; }
+ if ( decoy === 'blank' ) {
+ callArgs[0] = 'about:blank';
+ const r = context.reflect();
+ setTimeout(( ) => { r.close(); }, autoRemoveAfter);
+ return r;
+ }
+ const decoyElem = decoy === 'obj'
+ ? createDecoy('object', 'data', ...callArgs)
+ : createDecoy('iframe', 'src', ...callArgs);
+ let popup = decoyElem.contentWindow;
+ if ( typeof popup === 'object' && popup !== null ) {
+ Object.defineProperty(popup, 'closed', { value: false });
+ } else {
+ popup = new Proxy(self, {
+ get: function(target, prop, ...args) {
+ if ( prop === 'closed' ) { return false; }
+ const r = Reflect.get(target, prop, ...args);
+ if ( typeof r === 'function' ) { return noopFunc; }
+ return r;
+ },
+ set: function(...args) {
+ return Reflect.set(...args);
+ },
+ });
+ }
+ if ( safe.logLevel !== 0 ) {
+ popup = new Proxy(popup, {
+ get: function(target, prop, ...args) {
+ const r = Reflect.get(target, prop, ...args);
+ safe.uboLog(logPrefix, `popup / get ${prop} === ${r}`);
+ if ( typeof r === 'function' ) {
+ return (...args) => { return r.call(target, ...args); };
+ }
+ return r;
+ },
+ set: function(target, prop, value, ...args) {
+ safe.uboLog(logPrefix, `popup / set ${prop} = ${value}`);
+ return Reflect.set(target, prop, value, ...args);
+ },
+ });
+ }
+ return popup;
+ });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'close-window.js',
+ aliases: [
+ 'window-close-if.js',
+ ],
+ fn: closeWindow,
+ world: 'ISOLATED',
+ dependencies: [
+ 'safe-self.fn',
+ ],
+});
+// https://github.com/uBlockOrigin/uAssets/issues/10323#issuecomment-992312847
+// https://github.com/AdguardTeam/Scriptlets/issues/158
+// https://github.com/uBlockOrigin/uBlock-issues/discussions/2270
+function closeWindow(
+ arg1 = ''
+) {
+ if ( typeof arg1 !== 'string' ) { return; }
+ const safe = safeSelf();
+ let subject = '';
+ if ( /^\/.*\/$/.test(arg1) ) {
+ subject = window.location.href;
+ } else if ( arg1 !== '' ) {
+ subject = `${window.location.pathname}${window.location.search}`;
+ }
+ try {
+ const re = safe.patternToRegex(arg1);
+ if ( re.test(subject) ) {
+ window.close();
+ }
+ } catch(ex) {
+ console.log(ex);
+ }
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'window.name-defuser.js',
+ fn: windowNameDefuser,
+});
+// https://github.com/gorhill/uBlock/issues/1228
+function windowNameDefuser() {
+ if ( window === window.top ) {
+ window.name = '';
+ }
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'overlay-buster.js',
+ fn: overlayBuster,
+});
+// Experimental: Generic nuisance overlay buster.
+// if this works well and proves to be useful, this may end up
+// as a stock tool in uBO's popup panel.
+function overlayBuster(allFrames) {
+ if ( allFrames === '' && window !== window.top ) { return; }
+ var tstart;
+ var ttl = 30000;
+ var delay = 0;
+ var delayStep = 50;
+ var buster = function() {
+ var docEl = document.documentElement,
+ bodyEl = document.body,
+ vw = Math.min(docEl.clientWidth, window.innerWidth),
+ vh = Math.min(docEl.clientHeight, window.innerHeight),
+ tol = Math.min(vw, vh) * 0.05,
+ el = document.elementFromPoint(vw/2, vh/2),
+ style, rect;
+ for (;;) {
+ if ( el === null || el.parentNode === null || el === bodyEl ) {
+ break;
+ }
+ style = window.getComputedStyle(el);
+ if ( parseInt(style.zIndex, 10) >= 1000 || style.position === 'fixed' ) {
+ rect = el.getBoundingClientRect();
+ if ( rect.left <= tol && rect.top <= tol && (vw - rect.right) <= tol && (vh - rect.bottom) < tol ) {
+ el.parentNode.removeChild(el);
+ tstart = Date.now();
+ el = document.elementFromPoint(vw/2, vh/2);
+ bodyEl.style.setProperty('overflow', 'auto', 'important');
+ docEl.style.setProperty('overflow', 'auto', 'important');
+ continue;
+ }
+ }
+ el = el.parentNode;
+ }
+ if ( (Date.now() - tstart) < ttl ) {
+ delay = Math.min(delay + delayStep, 1000);
+ setTimeout(buster, delay);
+ }
+ };
+ var domReady = function(ev) {
+ if ( ev ) {
+ document.removeEventListener(ev.type, domReady);
+ }
+ tstart = Date.now();
+ setTimeout(buster, delay);
+ };
+ if ( document.readyState === 'loading' ) {
+ document.addEventListener('DOMContentLoaded', domReady);
+ } else {
+ domReady();
+ }
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'alert-buster.js',
+ fn: alertBuster,
+});
+// https://github.com/uBlockOrigin/uAssets/issues/8
+function alertBuster() {
+ window.alert = new Proxy(window.alert, {
+ apply: function(a) {
+ console.info(a);
+ },
+ get(target, prop) {
+ if ( prop === 'toString' ) {
+ return target.toString.bind(target);
+ }
+ return Reflect.get(target, prop);
+ },
+ });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'nowebrtc.js',
+ fn: noWebrtc,
+});
+// Prevent web pages from using RTCPeerConnection(), and report attempts in console.
+function noWebrtc() {
+ var rtcName = window.RTCPeerConnection ? 'RTCPeerConnection' : (
+ window.webkitRTCPeerConnection ? 'webkitRTCPeerConnection' : ''
+ );
+ if ( rtcName === '' ) { return; }
+ var log = console.log.bind(console);
+ var pc = function(cfg) {
+ log('Document tried to create an RTCPeerConnection: %o', cfg);
+ };
+ const noop = function() {
+ };
+ pc.prototype = {
+ close: noop,
+ createDataChannel: noop,
+ createOffer: noop,
+ setRemoteDescription: noop,
+ toString: function() {
+ return '[object RTCPeerConnection]';
+ }
+ };
+ var z = window[rtcName];
+ window[rtcName] = pc.bind(window);
+ if ( z.prototype ) {
+ z.prototype.createDataChannel = function() {
+ return {
+ close: function() {},
+ send: function() {}
+ };
+ }.bind(null);
+ }
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'disable-newtab-links.js',
+ fn: disableNewtabLinks,
+});
+// https://github.com/uBlockOrigin/uAssets/issues/913
+function disableNewtabLinks() {
+ document.addEventListener('click', ev => {
+ let target = ev.target;
+ while ( target !== null ) {
+ if ( target.localName === 'a' && target.hasAttribute('target') ) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ break;
+ }
+ target = target.parentNode;
+ }
+ }, { capture: true });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'xml-prune.js',
+ fn: xmlPrune,
+ dependencies: [
+ 'modify-xhr-response.fn',
+ 'safe-self.fn',
+ ],
+});
+function xmlPrune(
+ selector = '',
+ selectorCheck = '',
+ urlPattern = '',
+ ...varargs
+) {
+ if ( typeof selector !== 'string' ) { return; }
+ if ( selector === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('xml-prune', selector, selectorCheck, urlPattern);
+ const reUrl = safe.patternToRegex(urlPattern);
+ const extraArgs = safe.parseVarargs(varargs);
+ const queryAll = (xmlDoc, selector) => {
+ const isXpath = /^xpath\(.+\)$/.test(selector);
+ if ( isXpath === false ) {
+ return Array.from(xmlDoc.querySelectorAll(selector));
+ }
+ const xpr = xmlDoc.evaluate(
+ selector.slice(6, -1),
+ xmlDoc,
+ null,
+ XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
+ null
+ );
+ const out = [];
+ for ( let i = 0; i < xpr.snapshotLength; i++ ) {
+ const node = xpr.snapshotItem(i);
+ out.push(node);
+ }
+ return out;
+ };
+ const pruneFromDoc = xmlDoc => {
+ try {
+ if ( selectorCheck !== '' && xmlDoc.querySelector(selectorCheck) === null ) {
+ return xmlDoc;
+ }
+ if ( extraArgs.logdoc ) {
+ const serializer = new XMLSerializer();
+ safe.uboLog(logPrefix, `Document is\n\t${serializer.serializeToString(xmlDoc)}`);
+ }
+ const items = queryAll(xmlDoc, selector);
+ if ( items.length === 0 ) { return xmlDoc; }
+ safe.uboLog(logPrefix, `Removing ${items.length} items`);
+ for ( const item of items ) {
+ if ( item.nodeType === 1 ) {
+ item.remove();
+ } else if ( item.nodeType === 2 ) {
+ item.ownerElement.removeAttribute(item.nodeName);
+ }
+ safe.uboLog(logPrefix, `${item.constructor.name}.${item.nodeName} removed`);
+ }
+ } catch(ex) {
+ safe.uboErr(logPrefix, `Error: ${ex}`);
+ }
+ return xmlDoc;
+ };
+ const pruneFromText = text => {
+ if ( (/^\s*\s*$/.test(text)) === false ) {
+ return text;
+ }
+ try {
+ const xmlParser = new DOMParser();
+ const xmlDoc = xmlParser.parseFromString(text, 'text/xml');
+ pruneFromDoc(xmlDoc);
+ const serializer = new XMLSerializer();
+ text = serializer.serializeToString(xmlDoc);
+ } catch {
+ }
+ return text;
+ };
+ const urlFromArg = arg => {
+ if ( typeof arg === 'string' ) { return arg; }
+ if ( arg instanceof Request ) { return arg.url; }
+ return String(arg);
+ };
+ self.fetch = new Proxy(self.fetch, {
+ apply: function(target, thisArg, args) {
+ const fetchPromise = Reflect.apply(target, thisArg, args);
+ if ( reUrl.test(urlFromArg(args[0])) === false ) {
+ return fetchPromise;
+ }
+ return fetchPromise.then(responseBefore => {
+ const response = responseBefore.clone();
+ return response.text().then(text => {
+ const responseAfter = new Response(pruneFromText(text), {
+ status: responseBefore.status,
+ statusText: responseBefore.statusText,
+ headers: responseBefore.headers,
+ });
+ Object.defineProperties(responseAfter, {
+ ok: { value: responseBefore.ok },
+ redirected: { value: responseBefore.redirected },
+ type: { value: responseBefore.type },
+ url: { value: responseBefore.url },
+ });
+ return responseAfter;
+ }).catch(( ) =>
+ responseBefore
+ );
+ });
+ }
+ });
+ modifyXhrResponseFn(urlPattern, (xhr, before) => {
+ if ( before instanceof XMLDocument ) {
+ return pruneFromDoc(before);
+ }
+ if ( typeof before === 'string' ) {
+ return pruneFromText(before);
+ }
+ return before;
+ });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'm3u-prune.js',
+ fn: m3uPrune,
+ dependencies: [
+ 'proxy-apply.fn',
+ 'safe-self.fn',
+ ],
+});
+// https://en.wikipedia.org/wiki/M3U
+function m3uPrune(
+ m3uPattern = '',
+ urlPattern = ''
+) {
+ if ( typeof m3uPattern !== 'string' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('m3u-prune', m3uPattern, urlPattern);
+ const toLog = [];
+ const regexFromArg = arg => {
+ if ( arg === '' ) { return /^/; }
+ const match = /^\/(.+)\/([gms]*)$/.exec(arg);
+ if ( match !== null ) {
+ let flags = match[2] || '';
+ if ( flags.includes('m') ) { flags += 's'; }
+ return new RegExp(match[1], flags);
+ }
+ return new RegExp(
+ arg.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*+/g, '.*?')
+ );
+ };
+ const reM3u = regexFromArg(m3uPattern);
+ const reUrl = regexFromArg(urlPattern);
+ const pruneSpliceoutBlock = (lines, i) => {
+ if ( lines[i].startsWith('#EXT-X-CUE:TYPE="SpliceOut"') === false ) {
+ return false;
+ }
+ toLog.push(`\t${lines[i]}`);
+ lines[i] = undefined; i += 1;
+ if ( lines[i].startsWith('#EXT-X-ASSET:CAID') ) {
+ toLog.push(`\t${lines[i]}`);
+ lines[i] = undefined; i += 1;
+ }
+ if ( lines[i].startsWith('#EXT-X-SCTE35:') ) {
+ toLog.push(`\t${lines[i]}`);
+ lines[i] = undefined; i += 1;
+ }
+ if ( lines[i].startsWith('#EXT-X-CUE-IN') ) {
+ toLog.push(`\t${lines[i]}`);
+ lines[i] = undefined; i += 1;
+ }
+ if ( lines[i].startsWith('#EXT-X-SCTE35:') ) {
+ toLog.push(`\t${lines[i]}`);
+ lines[i] = undefined; i += 1;
+ }
+ return true;
+ };
+ const pruneInfBlock = (lines, i) => {
+ if ( lines[i].startsWith('#EXTINF') === false ) { return false; }
+ if ( reM3u.test(lines[i+1]) === false ) { return false; }
+ toLog.push('Discarding', `\t${lines[i]}, \t${lines[i+1]}`);
+ lines[i] = lines[i+1] = undefined; i += 2;
+ if ( lines[i].startsWith('#EXT-X-DISCONTINUITY') ) {
+ toLog.push(`\t${lines[i]}`);
+ lines[i] = undefined; i += 1;
+ }
+ return true;
+ };
+ const pruner = text => {
+ if ( (/^\s*#EXTM3U/.test(text)) === false ) { return text; }
+ if ( m3uPattern === '' ) {
+ safe.uboLog(` Content:\n${text}`);
+ return text;
+ }
+ if ( reM3u.multiline ) {
+ reM3u.lastIndex = 0;
+ for (;;) {
+ const match = reM3u.exec(text);
+ if ( match === null ) { break; }
+ let discard = match[0];
+ let before = text.slice(0, match.index);
+ if (
+ /^[\n\r]+/.test(discard) === false &&
+ /[\n\r]+$/.test(before) === false
+ ) {
+ const startOfLine = /[^\n\r]+$/.exec(before);
+ if ( startOfLine !== null ) {
+ before = before.slice(0, startOfLine.index);
+ discard = startOfLine[0] + discard;
+ }
+ }
+ let after = text.slice(match.index + match[0].length);
+ if (
+ /[\n\r]+$/.test(discard) === false &&
+ /^[\n\r]+/.test(after) === false
+ ) {
+ const endOfLine = /^[^\n\r]+/.exec(after);
+ if ( endOfLine !== null ) {
+ after = after.slice(endOfLine.index);
+ discard += discard + endOfLine[0];
+ }
+ }
+ text = before.trim() + '\n' + after.trim();
+ reM3u.lastIndex = before.length + 1;
+ toLog.push('Discarding', ...safe.String_split.call(discard, /\n+/).map(s => `\t${s}`));
+ if ( reM3u.global === false ) { break; }
+ }
+ return text;
+ }
+ const lines = safe.String_split.call(text, /\n\r|\n|\r/);
+ for ( let i = 0; i < lines.length; i++ ) {
+ if ( lines[i] === undefined ) { continue; }
+ if ( pruneSpliceoutBlock(lines, i) ) { continue; }
+ if ( pruneInfBlock(lines, i) ) { continue; }
+ }
+ return lines.filter(l => l !== undefined).join('\n');
+ };
+ const urlFromArg = arg => {
+ if ( typeof arg === 'string' ) { return arg; }
+ if ( arg instanceof Request ) { return arg.url; }
+ return String(arg);
+ };
+ proxyApplyFn('fetch', async function fetch(context) {
+ const args = context.callArgs;
+ const fetchPromise = context.reflect();
+ if ( reUrl.test(urlFromArg(args[0])) === false ) { return fetchPromise; }
+ const responseBefore = await fetchPromise;
+ const responseClone = responseBefore.clone();
+ const textBefore = await responseClone.text();
+ const textAfter = pruner(textBefore);
+ if ( textAfter === textBefore ) { return responseBefore; }
+ const responseAfter = new Response(textAfter, {
+ status: responseBefore.status,
+ statusText: responseBefore.statusText,
+ headers: responseBefore.headers,
+ });
+ Object.defineProperties(responseAfter, {
+ url: { value: responseBefore.url },
+ type: { value: responseBefore.type },
+ });
+ if ( toLog.length !== 0 ) {
+ toLog.unshift(logPrefix);
+ safe.uboLog(toLog.join('\n'));
+ }
+ return responseAfter;
+ })
+ self.XMLHttpRequest.prototype.open = new Proxy(self.XMLHttpRequest.prototype.open, {
+ apply: async (target, thisArg, args) => {
+ if ( reUrl.test(urlFromArg(args[1])) === false ) {
+ return Reflect.apply(target, thisArg, args);
+ }
+ thisArg.addEventListener('readystatechange', function() {
+ if ( thisArg.readyState !== 4 ) { return; }
+ const type = thisArg.responseType;
+ if ( type !== '' && type !== 'text' ) { return; }
+ const textin = thisArg.responseText;
+ const textout = pruner(textin);
+ if ( textout === textin ) { return; }
+ Object.defineProperty(thisArg, 'response', { value: textout });
+ Object.defineProperty(thisArg, 'responseText', { value: textout });
+ if ( toLog.length !== 0 ) {
+ toLog.unshift(logPrefix);
+ safe.uboLog(toLog.join('\n'));
+ }
+ });
+ return Reflect.apply(target, thisArg, args);
+ }
+ });
+}
+
+/*******************************************************************************
+ *
+ * @scriptlet call-nothrow
+ *
+ * @description
+ * Prevent a function call from throwing. The function will be called, however
+ * should it throw, the scriptlet will silently process the exception and
+ * returns as if no exception has occurred.
+ *
+ * ### Syntax
+ *
+ * ```text
+ * example.org##+js(call-nothrow, propertyChain)
+ * ```
+ *
+ * - `propertyChain`: a chain of dot-separated properties which leads to the
+ * function to be trapped.
+ *
+ * ### Examples
+ *
+ * example.org##+js(call-nothrow, Object.defineProperty)
+ *
+ * */
+
+builtinScriptlets.push({
+ name: 'call-nothrow.js',
+ fn: callNothrow,
+ dependencies: [
+ 'safe-self.fn',
+ ],
+});
+function callNothrow(
+ chain = ''
+) {
+ if ( typeof chain !== 'string' ) { return; }
+ if ( chain === '' ) { return; }
+ const safe = safeSelf();
+ const parts = safe.String_split.call(chain, '.');
+ let owner = window, prop;
+ for (;;) {
+ prop = parts.shift();
+ if ( parts.length === 0 ) { break; }
+ owner = owner[prop];
+ if ( owner instanceof Object === false ) { return; }
+ }
+ if ( prop === '' ) { return; }
+ const fn = owner[prop];
+ if ( typeof fn !== 'function' ) { return; }
+ owner[prop] = new Proxy(fn, {
+ apply: function(...args) {
+ let r;
+ try {
+ r = Reflect.apply(...args);
+ } catch {
+ }
+ return r;
+ },
+ });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'remove-node-text.js',
+ aliases: [
+ 'rmnt.js',
+ ],
+ fn: removeNodeText,
+ world: 'ISOLATED',
+ dependencies: [
+ 'replace-node-text.fn',
+ ],
+});
+function removeNodeText(
+ nodeName,
+ includes,
+ ...extraArgs
+) {
+ replaceNodeTextFn(nodeName, '', '', 'includes', includes || '', ...extraArgs);
+}
+
+/*******************************************************************************
+ *
+ * @scriptlet prevent-canvas
+ *
+ * @description
+ * Prevent usage of specific or all (default) canvas APIs.
+ *
+ * ### Syntax
+ *
+ * ```text
+ * example.com##+js(prevent-canvas [, contextType])
+ * ```
+ *
+ * - `contextType`: A specific type of canvas API to prevent (default to all
+ * APIs). Can be a string or regex which will be matched against the type
+ * used in getContext() call. Prepend with `!` to test for no-match.
+ *
+ * ### Examples
+ *
+ * 1. Prevent `example.com` from accessing all canvas APIs
+ *
+ * ```adblock
+ * example.com##+js(prevent-canvas)
+ * ```
+ *
+ * 2. Prevent access to any flavor of WebGL API, everywhere
+ *
+ * ```adblock
+ * *##+js(prevent-canvas, /webgl/)
+ * ```
+ *
+ * 3. Prevent `example.com` from accessing any flavor of canvas API except `2d`
+ *
+ * ```adblock
+ * example.com##+js(prevent-canvas, !2d)
+ * ```
+ *
+ * ### References
+ *
+ * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
+ *
+ * */
+
+builtinScriptlets.push({
+ name: 'prevent-canvas.js',
+ fn: preventCanvas,
+ dependencies: [
+ 'safe-self.fn',
+ ],
+});
+function preventCanvas(
+ contextType = ''
+) {
+ const safe = safeSelf();
+ const pattern = safe.initPattern(contextType, { canNegate: true });
+ const proto = globalThis.HTMLCanvasElement.prototype;
+ proto.getContext = new Proxy(proto.getContext, {
+ apply(target, thisArg, args) {
+ if ( safe.testPattern(pattern, args[0]) ) { return null; }
+ return Reflect.apply(target, thisArg, args);
+ }
+ });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'multiup.js',
+ fn: multiup,
+ world: 'ISOLATED',
+});
+function multiup() {
+ const handler = ev => {
+ const target = ev.target;
+ if ( target.matches('button[link]') === false ) { return; }
+ const ancestor = target.closest('form');
+ if ( ancestor === null ) { return; }
+ if ( ancestor !== target.parentElement ) { return; }
+ const link = (target.getAttribute('link') || '').trim();
+ if ( link === '' ) { return; }
+ ev.preventDefault();
+ ev.stopPropagation();
+ document.location.href = link;
+ };
+ document.addEventListener('click', handler, { capture: true });
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'break-on-call.js',
+ fn: breakOnCall,
+ dependencies: [
+ 'proxy-apply.fn',
+ ],
+});
+function breakOnCall(target) {
+ proxyApplyFn(target, function(context) {
+ debugger; // eslint-disable-line no-debugger
+ return context.reflect();
+ });
+}
+
+
+
+
+
+/*******************************************************************************
+ *
+ * Scriplets below this section are only available for filter lists from
+ * trusted sources. They all have the property `requiresTrust` set to `true`.
+ *
+ * Trusted sources are:
+ *
+ * - uBO's own filter lists, which name starts with "uBlock filters – ", and
+ * maintained at: https://github.com/uBlockOrigin/uAssets
+ *
+ * - The user's own filters as seen in "My filters" pane in uBO's dashboard.
+ *
+ * The trustworthiness of filters using these privileged scriptlets are
+ * evaluated at filter list compiled time: when a filter using one of the
+ * privileged scriptlet originates from a non-trusted filter list source, it
+ * is discarded at compile time, specifically from within:
+ *
+ * - Source: ./src/js/scriptlet-filtering.js
+ * - Method: scriptletFilteringEngine.compile(), via normalizeRawFilter()
+ *
+ **/
+
+/*******************************************************************************
+ *
+ * replace-node-text.js
+ *
+ * Replace text instance(s) with another text instance inside specific
+ * DOM nodes. By default, the scriplet stops and quits at the interactive
+ * stage of a document.
+ *
+ * See commit messages for usage:
+ * - https://github.com/gorhill/uBlock/commit/99ce027fd702
+ * - https://github.com/gorhill/uBlock/commit/41876336db48
+ *
+ **/
+
+builtinScriptlets.push({
+ name: 'trusted-replace-node-text.js',
+ requiresTrust: true,
+ aliases: [
+ 'trusted-rpnt.js',
+ 'replace-node-text.js',
+ 'rpnt.js',
+ ],
+ fn: replaceNodeText,
+ world: 'ISOLATED',
+ dependencies: [
+ 'replace-node-text.fn',
+ ],
+});
+function replaceNodeText(
+ nodeName,
+ pattern,
+ replacement,
+ ...extraArgs
+) {
+ replaceNodeTextFn(nodeName, pattern, replacement, ...extraArgs);
+}
+
+/*******************************************************************************
+ *
+ * trusted-replace-fetch-response.js
+ *
+ * Replaces response text content of fetch requests if all given parameters
+ * match.
+ *
+ * Reference:
+ * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-replace-fetch-response.js
+ *
+ **/
+
+builtinScriptlets.push({
+ name: 'trusted-replace-fetch-response.js',
+ requiresTrust: true,
+ aliases: [
+ 'trusted-rpfr.js',
+ ],
+ fn: trustedReplaceFetchResponse,
+ dependencies: [
+ 'replace-fetch-response.fn',
+ ],
+});
+function trustedReplaceFetchResponse(...args) {
+ replaceFetchResponseFn(true, ...args);
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'trusted-replace-xhr-response.js',
+ requiresTrust: true,
+ fn: trustedReplaceXhrResponse,
+ dependencies: [
+ 'match-object-properties.fn',
+ 'parse-properties-to-match.fn',
+ 'safe-self.fn',
+ ],
+});
+function trustedReplaceXhrResponse(
+ pattern = '',
+ replacement = '',
+ propsToMatch = '',
+ ...varargs
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('trusted-replace-xhr-response', pattern, replacement, propsToMatch);
+ const xhrInstances = new WeakMap();
+ if ( pattern === '*' ) { pattern = '.*'; }
+ const rePattern = safe.patternToRegex(pattern);
+ const propNeedles = parsePropertiesToMatchFn(propsToMatch, 'url');
+ const extraArgs = safe.parseVarargs(varargs);
+ const reIncludes = extraArgs.includes ? safe.patternToRegex(extraArgs.includes) : null;
+ self.XMLHttpRequest = class extends self.XMLHttpRequest {
+ open(method, url, ...args) {
+ const outerXhr = this;
+ const xhrDetails = { method, url };
+ let outcome = 'match';
+ if ( propNeedles.size !== 0 ) {
+ if ( matchObjectPropertiesFn(propNeedles, xhrDetails) === undefined ) {
+ outcome = 'nomatch';
+ }
+ }
+ if ( outcome === 'match' ) {
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, `Matched "propsToMatch"`);
+ }
+ xhrInstances.set(outerXhr, xhrDetails);
+ }
+ return super.open(method, url, ...args);
+ }
+ get response() {
+ const innerResponse = super.response;
+ const xhrDetails = xhrInstances.get(this);
+ if ( xhrDetails === undefined ) {
+ return innerResponse;
+ }
+ const responseLength = typeof innerResponse === 'string'
+ ? innerResponse.length
+ : undefined;
+ if ( xhrDetails.lastResponseLength !== responseLength ) {
+ xhrDetails.response = undefined;
+ xhrDetails.lastResponseLength = responseLength;
+ }
+ if ( xhrDetails.response !== undefined ) {
+ return xhrDetails.response;
+ }
+ if ( typeof innerResponse !== 'string' ) {
+ return (xhrDetails.response = innerResponse);
+ }
+ if ( reIncludes && reIncludes.test(innerResponse) === false ) {
+ return (xhrDetails.response = innerResponse);
+ }
+ const textBefore = innerResponse;
+ const textAfter = textBefore.replace(rePattern, replacement);
+ if ( textAfter !== textBefore ) {
+ safe.uboLog(logPrefix, 'Match');
+ }
+ return (xhrDetails.response = textAfter);
+ }
+ get responseText() {
+ const response = this.response;
+ if ( typeof response !== 'string' ) {
+ return super.responseText;
+ }
+ return response;
+ }
+ };
+}
+
+/*******************************************************************************
+ *
+ * @scriptlet trusted-click-element
+ *
+ * @description
+ * Programmatically click on one or more elements.
+ *
+ * @param steps
+ * A comma-separated list of steps to fulfilled:
+ * - A valid CSS selector matching an element to programmatically click
+ * - A integer: A delay in milliseconds to wait before the next step is
+ * processed. If the last step is an integer, it is used as a timeout value
+ * before the scriptlet bails out (default to 11s)
+ * If the list of steps starts with `;` or `|`, it will be used as the
+ * separator character instead of comma. This is convenient when a selector
+ * contains a comma character.
+ * A selector must be one of:
+ * - A valid plain CSS selector
+ * - An xpath expression: xpath:[expression]
+ * Addtionally:
+ * - Use `>>>` between selectors to target an element inside a shadow root
+ * - Prepend with `when-visible:[selector]` to progrmatically click the element
+ * only when it is visible
+ *
+ **/
+
+builtinScriptlets.push({
+ name: 'trusted-click-element.js',
+ requiresTrust: true,
+ fn: trustedClickElement,
+ world: 'ISOLATED',
+ dependencies: [
+ 'get-all-cookies.fn',
+ 'get-all-local-storage.fn',
+ 'lookup-elements.fn',
+ 'run-at-html-element.fn',
+ 'safe-self.fn',
+ ],
+});
+function trustedClickElement(
+ selectors = '',
+ extraMatch = '',
+ delay = ''
+) {
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('trusted-click-element', selectors, extraMatch, delay);
+
+ if ( extraMatch !== '' ) {
+ const assertions = safe.String_split.call(extraMatch, ',').map(s => {
+ const pos1 = s.indexOf(':');
+ const s1 = pos1 !== -1 ? s.slice(0, pos1) : s;
+ const not = s1.startsWith('!');
+ const type = not ? s1.slice(1) : s1;
+ const s2 = pos1 !== -1 ? s.slice(pos1+1).trim() : '';
+ if ( s2 === '' ) { return; }
+ const out = { not, type };
+ const match = /^\/(.+)\/(i?)$/.exec(s2);
+ if ( match !== null ) {
+ out.re = new RegExp(match[1], match[2] || undefined);
+ return out;
+ }
+ const pos2 = s2.indexOf('=');
+ const key = pos2 !== -1 ? s2.slice(0, pos2).trim() : s2;
+ const value = pos2 !== -1 ? s2.slice(pos2+1).trim() : '';
+ out.re = new RegExp(`^${safe.escapeRegexChars(key)}=${safe.escapeRegexChars(value)}`);
+ return out;
+ }).filter(details => details !== undefined);
+ const allCookies = assertions.some(o => o.type === 'cookie')
+ ? getAllCookiesFn()
+ : [];
+ const allStorageItems = assertions.some(o => o.type === 'localStorage')
+ ? getAllLocalStorageFn()
+ : [];
+ const hasNeedle = (haystack, needle) => {
+ for ( const { key, value } of haystack ) {
+ if ( needle.test(`${key}=${value}`) ) { return true; }
+ }
+ return false;
+ };
+ for ( const { not, type, re } of assertions ) {
+ switch ( type ) {
+ case 'cookie':
+ if ( hasNeedle(allCookies, re) === not ) { return; }
+ break;
+ case 'localStorage':
+ if ( hasNeedle(allStorageItems, re) === not ) { return; }
+ break;
+ }
+ }
+ }
+
+ const steps = (( ) => {
+ const steps = /^[;|]/.test(selectors)
+ ? safe.String_split.call(selectors.slice(1), selectors.charAt(0))
+ : safe.String_split.call(selectors, ',');
+ return steps.map(a => {
+ a = a.trim();
+ if ( /^\d+$/.test(a) ) { return parseInt(a, 10); }
+ return a;
+ });
+ })();
+ if ( steps.length === 0 ) { return; }
+ const clickDelay = parseInt(delay, 10) || 1;
+ for ( let i = steps.length-1; i > 0; i-- ) {
+ if ( typeof steps[i] !== 'string' ) { continue; }
+ if ( typeof steps[i-1] !== 'string' ) { continue; }
+ steps.splice(i, 0, clickDelay);
+ }
+ if ( steps.length === 1 && delay !== '' ) {
+ steps.unshift(clickDelay);
+ }
+ if ( typeof steps.at(-1) !== 'number' ) {
+ steps.push(11000);
+ }
+
+ const timeout = steps.pop();
+
+ const waitForTime = ms => {
+ return new Promise(resolve => {
+ safe.uboLog(logPrefix, `Waiting for ${ms} ms`);
+ waitForTime.timer = setTimeout(( ) => {
+ waitForTime.timer = undefined;
+ resolve();
+ }, ms);
+ });
+ };
+
+ const waitForElement = directive => {
+ safe.uboLog(logPrefix, `Waiting for ${directive}`);
+ return lookupElementsFn(directive, Date.now() + timeout).then(elems => {
+ if ( elems.length === 0 ) { return false; }
+ elems[0].click();
+ safe.uboLog(logPrefix, `Clicked ${directive}`);
+ return true;
+ });
+ };
+
+ const process = async ( ) => {
+ while ( steps.length !== 0 ) {
+ const step = steps.shift();
+ if ( step === undefined ) { break; }
+ if ( typeof step === 'number' ) {
+ await waitForTime(step);
+ if ( step === 1 ) { continue; }
+ continue;
+ }
+ if ( step.startsWith('!') ) { continue; }
+ const clicked = await waitForElement(step);
+ if ( clicked ) { continue; }
+ safe.uboLog(logPrefix, `Timed out waiting on ${step}`);
+ break;
+ }
+ };
+
+ runAtHtmlElementFn(process);
+}
+
+/******************************************************************************/
+
+builtinScriptlets.push({
+ name: 'trusted-replace-outbound-text.js',
+ requiresTrust: true,
+ fn: trustedReplaceOutboundText,
+ dependencies: [
+ 'proxy-apply.fn',
+ 'safe-self.fn',
+ ],
+});
+function trustedReplaceOutboundText(
+ propChain = '',
+ rawPattern = '',
+ rawReplacement = '',
+ ...varargs
+) {
+ if ( propChain === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('trusted-replace-outbound-text', propChain, rawPattern, rawReplacement, ...varargs);
+ const rePattern = safe.patternToRegex(rawPattern);
+ const replacement = rawReplacement.startsWith('json:')
+ ? safe.JSON_parse(rawReplacement.slice(5))
+ : rawReplacement;
+ const extraArgs = safe.parseVarargs(varargs);
+ const reCondition = safe.patternToRegex(extraArgs.condition || '');
+ proxyApplyFn(propChain, function(context) {
+ const encodedTextBefore = context.reflect();
+ let textBefore = encodedTextBefore;
+ if ( extraArgs.encoding === 'base64' ) {
+ try { textBefore = self.atob(encodedTextBefore); }
+ catch { return encodedTextBefore; }
+ }
+ if ( rawPattern === '' ) {
+ safe.uboLog(logPrefix, 'Decoded outbound text:\n', textBefore);
+ return encodedTextBefore;
+ }
+ reCondition.lastIndex = 0;
+ if ( reCondition.test(textBefore) === false ) { return encodedTextBefore; }
+ const textAfter = textBefore.replace(rePattern, replacement);
+ if ( textAfter === textBefore ) { return encodedTextBefore; }
+ safe.uboLog(logPrefix, 'Matched and replaced');
+ if ( safe.logLevel > 1 ) {
+ safe.uboLog(logPrefix, 'Modified decoded outbound text:\n', textAfter);
+ }
+ let encodedTextAfter = textAfter;
+ if ( extraArgs.encoding === 'base64' ) {
+ encodedTextAfter = self.btoa(textAfter);
+ }
+ return encodedTextAfter;
+ });
+}
+
+/*******************************************************************************
+ *
+ * Reference:
+ * https://github.com/AdguardTeam/Scriptlets/blob/5a92d79489/wiki/about-trusted-scriptlets.md#trusted-suppress-native-method
+ *
+ * This is a first version with current limitations:
+ * - Does not support matching arguments which are object or array
+ * - Does not support `stack` parameter
+ *
+ * If `signatureStr` parameter is not declared, the scriptlet will log all calls
+ * to `methodPath` along with the arguments passed and will not prevent the
+ * trapped method.
+ *
+ * */
+
+builtinScriptlets.push({
+ name: 'trusted-suppress-native-method.js',
+ requiresTrust: true,
+ fn: trustedSuppressNativeMethod,
+ dependencies: [
+ 'matches-stack-trace.fn',
+ 'proxy-apply.fn',
+ 'safe-self.fn',
+ ],
+});
+function trustedSuppressNativeMethod(
+ methodPath = '',
+ signature = '',
+ how = '',
+ stack = ''
+) {
+ if ( methodPath === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('trusted-suppress-native-method', methodPath, signature, how, stack);
+ const signatureArgs = safe.String_split.call(signature, /\s*\|\s*/).map(v => {
+ if ( /^".*"$/.test(v) ) {
+ return { type: 'pattern', re: safe.patternToRegex(v.slice(1, -1)) };
+ }
+ if ( /^\/.+\/$/.test(v) ) {
+ return { type: 'pattern', re: safe.patternToRegex(v) };
+ }
+ if ( v === 'false' ) {
+ return { type: 'exact', value: false };
+ }
+ if ( v === 'true' ) {
+ return { type: 'exact', value: true };
+ }
+ if ( v === 'null' ) {
+ return { type: 'exact', value: null };
+ }
+ if ( v === 'undefined' ) {
+ return { type: 'exact', value: undefined };
+ }
+ });
+ const stackNeedle = safe.initPattern(stack, { canNegate: true });
+ proxyApplyFn(methodPath, function(context) {
+ const { callArgs } = context;
+ if ( signature === '' ) {
+ safe.uboLog(logPrefix, `Arguments:\n${callArgs.join('\n')}`);
+ return context.reflect();
+ }
+ for ( let i = 0; i < signatureArgs.length; i++ ) {
+ const signatureArg = signatureArgs[i];
+ if ( signatureArg === undefined ) { continue; }
+ const targetArg = i < callArgs.length ? callArgs[i] : undefined;
+ if ( signatureArg.type === 'exact' ) {
+ if ( targetArg !== signatureArg.value ) {
+ return context.reflect();
+ }
+ }
+ if ( signatureArg.type === 'pattern' ) {
+ if ( safe.RegExp_test(signatureArg.re, targetArg) === false ) {
+ return context.reflect();
+ }
+ }
+ }
+ if ( stackNeedle.matchAll !== true ) {
+ const logLevel = safe.logLevel > 1 ? 'all' : '';
+ if ( matchesStackTraceFn(stackNeedle, logLevel) === false ) {
+ return context.reflect();
+ }
+ }
+ if ( how === 'debug' ) {
+ debugger; // eslint-disable-line no-debugger
+ return context.reflect();
+ }
+ safe.uboLog(logPrefix, `Suppressed:\n${callArgs.join('\n')}`);
+ if ( how === 'abort' ) {
+ throw new ReferenceError();
+ }
+ });
+}
+
+/**
+ * @trustedScriptlet trusted-prevent-dom-bypass
+ *
+ * @description
+ * Prevent the bypassing of uBO scriptlets through anonymous embedded context.
+ *
+ * Ensure that a target method in the embedded context is using the
+ * corresponding parent context's method (which is assumed to be
+ * properly patched), or to replace the embedded context with that of the
+ * parent context.
+ *
+ * Root issue:
+ * https://issues.chromium.org/issues/40202434
+ *
+ * @param methodPath
+ * The method which calls must be intercepted. The arguments
+ * of the intercepted calls are assumed to be HTMLElement, anything else will
+ * be ignored.
+ *
+ * @param [targetProps]
+ * The space-separated methods in the embedded context which should be
+ * delegated to the parent context. If no method is specified, the embedded
+ * context becomes the parent one, i.e. all properties of the embedded context
+ * will be that of the parent context.
+ *
+ * @example
+ * ##+js(trusted-prevent-dom-bypass, Element.prototype.append, open)
+ *
+ * @example
+ * ##+js(trusted-prevent-dom-bypass, Element.prototype.appendChild, XMLHttpRequest JSON.parse)
+ *
+ * */
+
+builtinScriptlets.push({
+ name: 'trusted-prevent-dom-bypass.js',
+ requiresTrust: true,
+ fn: trustedPreventDomBypass,
+ dependencies: [
+ 'proxy-apply.fn',
+ 'safe-self.fn',
+ ],
+});
+function trustedPreventDomBypass(
+ methodPath = '',
+ targetProp = ''
+) {
+ if ( methodPath === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('trusted-prevent-dom-bypass', methodPath, targetProp);
+ proxyApplyFn(methodPath, function(context) {
+ const elems = new Set(context.callArgs.filter(e => e instanceof HTMLElement));
+ const r = context.reflect();
+ if ( elems.length === 0 ) { return r; }
+ for ( const elem of elems ) {
+ try {
+ if ( `${elem.contentWindow}` !== '[object Window]' ) { continue; }
+ if ( elem.contentWindow.location.href !== 'about:blank' ) {
+ if ( elem.contentWindow.location.href !== self.location.href ) {
+ continue;
+ }
+ }
+ if ( targetProp !== '' ) {
+ const chains = targetProp.split(/\s+/);
+ let me = self, it = elem.contentWindow;
+ for ( let chain of chains ) {
+ for (;;) {
+ const pos = chain.indexOf('.');
+ if ( pos === -1 ) { break; }
+ const prop = chain.slice(0, pos);
+ me = me[prop]; it = it[prop];
+ chain = chain.slice(pos+1);
+ }
+ it[chain] = me[chain];
+ }
+ } else {
+ Object.defineProperty(elem, 'contentWindow', { value: self });
+ }
+ safe.uboLog(logPrefix, 'Bypass prevented');
+ } catch {
+ }
+ }
+ return r;
+ });
+}
+
+/**
+ * @trustedScriptlet trusted-override-element-method
+ *
+ * @description
+ * Override the behavior of a method on matching elements.
+ *
+ * @param methodPath
+ * The method which calls must be intercepted.
+ *
+ * @param [selector]
+ * A CSS selector which the target element must match. If not specified,
+ * the override will occur for all elements.
+ *
+ * @param [disposition]
+ * How the override should be handled. If not specified, the overridden call
+ * will be equivalent to an empty function. If set to `throw`, an exception
+ * will be thrown. Any other value will be validated and returned as a
+ * supported safe constant.
+ *
+ * @example
+ * ##+js(trusted-override-element-method, HTMLAnchorElement.prototype.click, a[target="_blank"][style])
+ *
+ * */
+
+builtinScriptlets.push({
+ name: 'trusted-override-element-method.js',
+ requiresTrust: true,
+ fn: trustedOverrideElementMethod,
+ dependencies: [
+ 'proxy-apply.fn',
+ 'safe-self.fn',
+ 'validate-constant.fn',
+ ],
+});
+function trustedOverrideElementMethod(
+ methodPath = '',
+ selector = '',
+ disposition = '',
+ ...varargs
+) {
+ if ( methodPath === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('trusted-override-element-method', methodPath, selector, disposition);
+ const extraArgs = safe.parseVarargs(varargs);
+ proxyApplyFn(methodPath, function(context) {
+ let override = selector === '';
+ if ( override === false ) {
+ const { thisArg } = context;
+ try {
+ override = thisArg.closest(selector) === thisArg;
+ } catch {
+ }
+ }
+ if ( override === false ) {
+ return context.reflect();
+ }
+ safe.uboLog(logPrefix, 'Overridden');
+ if ( disposition === '' ) { return; }
+ if ( disposition === 'debug' && safe.logLevel !== 0 ) {
+ debugger; // eslint-disable-line no-debugger
+ }
+ if ( disposition === 'throw' ) {
+ throw new ReferenceError();
+ }
+ return validateConstantFn(true, disposition, extraArgs);
+ });
+}
+
+/******************************************************************************/
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/set-constant.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/set-constant.js
new file mode 100644
index 00000000..ced3adf0
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/set-constant.js
@@ -0,0 +1,288 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { runAt } from './run-at.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+export function validateConstantFn(trusted, raw, extraArgs = {}) {
+ const safe = safeSelf();
+ let value;
+ if ( raw === 'undefined' ) {
+ value = undefined;
+ } else if ( raw === 'false' ) {
+ value = false;
+ } else if ( raw === 'true' ) {
+ value = true;
+ } else if ( raw === 'null' ) {
+ value = null;
+ } else if ( raw === "''" || raw === '' ) {
+ value = '';
+ } else if ( raw === '[]' || raw === 'emptyArr' ) {
+ value = [];
+ } else if ( raw === '{}' || raw === 'emptyObj' ) {
+ value = {};
+ } else if ( raw === 'noopFunc' ) {
+ value = function(){};
+ } else if ( raw === 'trueFunc' ) {
+ value = function(){ return true; };
+ } else if ( raw === 'falseFunc' ) {
+ value = function(){ return false; };
+ } else if ( raw === 'throwFunc' ) {
+ value = function(){ throw ''; };
+ } else if ( /^-?\d+$/.test(raw) ) {
+ value = parseInt(raw);
+ if ( isNaN(raw) ) { return; }
+ if ( Math.abs(raw) > 0x7FFF ) { return; }
+ } else if ( trusted ) {
+ if ( raw.startsWith('json:') ) {
+ try { value = safe.JSON_parse(raw.slice(5)); } catch { return; }
+ } else if ( raw.startsWith('{') && raw.endsWith('}') ) {
+ try { value = safe.JSON_parse(raw).value; } catch { return; }
+ }
+ } else {
+ return;
+ }
+ if ( extraArgs.as !== undefined ) {
+ if ( extraArgs.as === 'function' ) {
+ return ( ) => value;
+ } else if ( extraArgs.as === 'callback' ) {
+ return ( ) => (( ) => value);
+ } else if ( extraArgs.as === 'resolved' ) {
+ return Promise.resolve(value);
+ } else if ( extraArgs.as === 'rejected' ) {
+ return Promise.reject(value);
+ }
+ }
+ return value;
+}
+registerScriptlet(validateConstantFn, {
+ name: 'validate-constant.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+export function setConstantFn(
+ trusted = false,
+ chain = '',
+ rawValue = '',
+ ...varargs
+) {
+ if ( chain === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('set-constant', chain, rawValue);
+ const extraArgs = safe.parseVarargs(varargs);
+ function setConstant(chain, rawValue) {
+ const trappedProp = (( ) => {
+ const pos = chain.lastIndexOf('.');
+ if ( pos === -1 ) { return chain; }
+ return chain.slice(pos+1);
+ })();
+ const cloakFunc = fn => {
+ safe.Object_defineProperty(fn, 'name', { value: trappedProp });
+ return new Proxy(fn, {
+ defineProperty(target, prop) {
+ if ( prop !== 'toString' ) {
+ return Reflect.defineProperty(...arguments);
+ }
+ return true;
+ },
+ deleteProperty(target, prop) {
+ if ( prop !== 'toString' ) {
+ return Reflect.deleteProperty(...arguments);
+ }
+ return true;
+ },
+ get(target, prop) {
+ if ( prop === 'toString' ) {
+ return function() {
+ return `function ${trappedProp}() { [native code] }`;
+ }.bind(null);
+ }
+ return Reflect.get(...arguments);
+ },
+ });
+ };
+ if ( trappedProp === '' ) { return; }
+ const thisScript = document.currentScript;
+ let normalValue = validateConstantFn(trusted, rawValue, extraArgs);
+ if ( rawValue === 'noopFunc' || rawValue === 'trueFunc' || rawValue === 'falseFunc' ) {
+ normalValue = cloakFunc(normalValue);
+ }
+ let aborted = false;
+ const mustAbort = function(v) {
+ if ( trusted ) { return false; }
+ if ( aborted ) { return true; }
+ aborted =
+ (v !== undefined && v !== null) &&
+ (normalValue !== undefined && normalValue !== null) &&
+ (typeof v !== typeof normalValue);
+ if ( aborted ) {
+ safe.uboLog(logPrefix, `Aborted because value set to ${v}`);
+ }
+ return aborted;
+ };
+ // https://github.com/uBlockOrigin/uBlock-issues/issues/156
+ // Support multiple trappers for the same property.
+ const trapProp = function(owner, prop, configurable, handler) {
+ if ( handler.init(configurable ? owner[prop] : normalValue) === false ) { return; }
+ const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop);
+ let prevGetter, prevSetter;
+ if ( odesc instanceof safe.Object ) {
+ owner[prop] = normalValue;
+ if ( odesc.get instanceof Function ) {
+ prevGetter = odesc.get;
+ }
+ if ( odesc.set instanceof Function ) {
+ prevSetter = odesc.set;
+ }
+ }
+ try {
+ safe.Object_defineProperty(owner, prop, {
+ configurable,
+ get() {
+ if ( prevGetter !== undefined ) {
+ prevGetter();
+ }
+ return handler.getter();
+ },
+ set(a) {
+ if ( prevSetter !== undefined ) {
+ prevSetter(a);
+ }
+ handler.setter(a);
+ }
+ });
+ safe.uboLog(logPrefix, 'Trap installed');
+ } catch(ex) {
+ safe.uboErr(logPrefix, ex);
+ }
+ };
+ const trapChain = function(owner, chain) {
+ const pos = chain.indexOf('.');
+ if ( pos === -1 ) {
+ trapProp(owner, chain, false, {
+ v: undefined,
+ init: function(v) {
+ if ( mustAbort(v) ) { return false; }
+ this.v = v;
+ return true;
+ },
+ getter: function() {
+ if ( document.currentScript === thisScript ) {
+ return this.v;
+ }
+ safe.uboLog(logPrefix, 'Property read');
+ return normalValue;
+ },
+ setter: function(a) {
+ if ( mustAbort(a) === false ) { return; }
+ normalValue = a;
+ }
+ });
+ return;
+ }
+ const prop = chain.slice(0, pos);
+ const v = owner[prop];
+ chain = chain.slice(pos + 1);
+ if ( v instanceof safe.Object || typeof v === 'object' && v !== null ) {
+ trapChain(v, chain);
+ return;
+ }
+ trapProp(owner, prop, true, {
+ v: undefined,
+ init: function(v) {
+ this.v = v;
+ return true;
+ },
+ getter: function() {
+ return this.v;
+ },
+ setter: function(a) {
+ this.v = a;
+ if ( a instanceof safe.Object ) {
+ trapChain(a, chain);
+ }
+ }
+ });
+ };
+ trapChain(window, chain);
+ }
+ runAt(( ) => {
+ setConstant(chain, rawValue);
+ }, extraArgs.runAt);
+}
+registerScriptlet(setConstantFn, {
+ name: 'set-constant.fn',
+ dependencies: [
+ runAt,
+ safeSelf,
+ validateConstantFn,
+ ],
+});
+
+/******************************************************************************/
+
+export function setConstant(
+ ...args
+) {
+ setConstantFn(false, ...args);
+}
+registerScriptlet(setConstant, {
+ name: 'set-constant.js',
+ aliases: [
+ 'set.js',
+ ],
+ dependencies: [
+ setConstantFn,
+ ],
+});
+
+/*******************************************************************************
+ *
+ * trusted-set-constant.js
+ *
+ * Set specified property to any value. This is essentially the same as
+ * set-constant.js, but with no restriction as to which values can be used.
+ *
+ **/
+
+export function trustedSetConstant(
+ ...args
+) {
+ setConstantFn(true, ...args);
+}
+registerScriptlet(trustedSetConstant, {
+ name: 'trusted-set-constant.js',
+ requiresTrust: true,
+ aliases: [
+ 'trusted-set.js',
+ ],
+ dependencies: [
+ setConstantFn,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/shared.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/shared.js
new file mode 100644
index 00000000..01a9de71
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/shared.js
@@ -0,0 +1,40 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+// Code imported from main code base and exposed as injectable scriptlets
+import { ArglistParser as __ArglistParser__ } from '../arglist-parser.js';
+import { JSONPath as __JSONPath__ } from '../jsonpath.js';
+import { registerScriptlet } from './base.js';
+
+/******************************************************************************/
+
+export const ArglistParser = __ArglistParser__;
+
+registerScriptlet(ArglistParser, {
+ name: 'arglist-parser.fn',
+});
+
+export const JSONPath = __JSONPath__;
+
+registerScriptlet(JSONPath, {
+ name: 'jsonpath.fn',
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/spoof-css.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/spoof-css.js
new file mode 100644
index 00000000..7cc7b9f9
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/spoof-css.js
@@ -0,0 +1,163 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/**
+ * @scriptlet spoof-css.js
+ *
+ * @description
+ * Spoof the value of CSS properties.
+ *
+ * @param selector
+ * A CSS selector for the element(s) to target.
+ *
+ * @param [property, value, ...]
+ * A list of property-value pairs of the style properties to spoof to the
+ * specified values.
+*
+ * */
+
+export function spoofCSS(
+ selector,
+ ...args
+) {
+ if ( typeof selector !== 'string' ) { return; }
+ if ( selector === '' ) { return; }
+ const toCamelCase = s => s.replace(/-[a-z]/g, s => s.charAt(1).toUpperCase());
+ const propToValueMap = new Map();
+ const privatePropToValueMap = new Map();
+ for ( let i = 0; i < args.length; i += 2 ) {
+ const prop = toCamelCase(args[i+0]);
+ if ( prop === '' ) { break; }
+ const value = args[i+1];
+ if ( typeof value !== 'string' ) { break; }
+ if ( prop.charCodeAt(0) === 0x5F /* _ */ ) {
+ privatePropToValueMap.set(prop, value);
+ } else {
+ propToValueMap.set(prop, value);
+ }
+ }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('spoof-css', selector, ...args);
+ const instanceProperties = [ 'cssText', 'length', 'parentRule' ];
+ const spoofStyle = (prop, real) => {
+ const normalProp = toCamelCase(prop);
+ const shouldSpoof = propToValueMap.has(normalProp);
+ const value = shouldSpoof ? propToValueMap.get(normalProp) : real;
+ if ( shouldSpoof ) {
+ safe.uboLog(logPrefix, `Spoofing ${prop} to ${value}`);
+ }
+ return value;
+ };
+ const cloackFunc = (fn, thisArg, name) => {
+ const trap = fn.bind(thisArg);
+ Object.defineProperty(trap, 'name', { value: name });
+ Object.defineProperty(trap, 'toString', {
+ value: ( ) => `function ${name}() { [native code] }`
+ });
+ return trap;
+ };
+ self.getComputedStyle = new Proxy(self.getComputedStyle, {
+ apply: function(target, thisArg, args) {
+ // eslint-disable-next-line no-debugger
+ if ( privatePropToValueMap.has('_debug') ) { debugger; }
+ const style = Reflect.apply(target, thisArg, args);
+ const targetElements = new WeakSet(document.querySelectorAll(selector));
+ if ( targetElements.has(args[0]) === false ) { return style; }
+ const proxiedStyle = new Proxy(style, {
+ get(target, prop) {
+ if ( typeof target[prop] === 'function' ) {
+ if ( prop === 'getPropertyValue' ) {
+ return cloackFunc(function getPropertyValue(prop) {
+ return spoofStyle(prop, target[prop]);
+ }, target, 'getPropertyValue');
+ }
+ return cloackFunc(target[prop], target, prop);
+ }
+ if ( instanceProperties.includes(prop) ) {
+ return Reflect.get(target, prop);
+ }
+ return spoofStyle(prop, Reflect.get(target, prop));
+ },
+ getOwnPropertyDescriptor(target, prop) {
+ if ( propToValueMap.has(prop) ) {
+ return {
+ configurable: true,
+ enumerable: true,
+ value: propToValueMap.get(prop),
+ writable: true,
+ };
+ }
+ return Reflect.getOwnPropertyDescriptor(target, prop);
+ },
+ });
+ return proxiedStyle;
+ },
+ get(target, prop) {
+ if ( prop === 'toString' ) {
+ return target.toString.bind(target);
+ }
+ return Reflect.get(target, prop);
+ },
+ });
+ Element.prototype.getBoundingClientRect = new Proxy(Element.prototype.getBoundingClientRect, {
+ apply: function(target, thisArg, args) {
+ // eslint-disable-next-line no-debugger
+ if ( privatePropToValueMap.has('_debug') ) { debugger; }
+ const rect = Reflect.apply(target, thisArg, args);
+ const targetElements = new WeakSet(document.querySelectorAll(selector));
+ if ( targetElements.has(thisArg) === false ) { return rect; }
+ let { x, y, height, width } = rect;
+ if ( privatePropToValueMap.has('_rectx') ) {
+ x = parseFloat(privatePropToValueMap.get('_rectx'));
+ }
+ if ( privatePropToValueMap.has('_recty') ) {
+ y = parseFloat(privatePropToValueMap.get('_recty'));
+ }
+ if ( privatePropToValueMap.has('_rectw') ) {
+ width = parseFloat(privatePropToValueMap.get('_rectw'));
+ } else if ( propToValueMap.has('width') ) {
+ width = parseFloat(propToValueMap.get('width'));
+ }
+ if ( privatePropToValueMap.has('_recth') ) {
+ height = parseFloat(privatePropToValueMap.get('_recth'));
+ } else if ( propToValueMap.has('height') ) {
+ height = parseFloat(propToValueMap.get('height'));
+ }
+ return new self.DOMRect(x, y, width, height);
+ },
+ get(target, prop) {
+ if ( prop === 'toString' ) {
+ return target.toString.bind(target);
+ }
+ return Reflect.get(target, prop);
+ },
+ });
+}
+registerScriptlet(spoofCSS, {
+ name: 'spoof-css.js',
+ dependencies: [
+ safeSelf,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/stack-trace.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/stack-trace.js
new file mode 100644
index 00000000..7d742135
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/stack-trace.js
@@ -0,0 +1,149 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { getExceptionTokenFn } from './utils.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+export function matchesStackTraceFn(
+ needleDetails,
+ logLevel = ''
+) {
+ const safe = safeSelf();
+ const exceptionToken = getExceptionTokenFn();
+ const error = new safe.Error(exceptionToken);
+ const docURL = new URL(self.location.href);
+ docURL.hash = '';
+ // Normalize stack trace
+ const reLine = /(.*?@)?(\S+)(:\d+):\d+\)?$/;
+ const lines = [];
+ for ( let line of safe.String_split.call(error.stack, /[\n\r]+/) ) {
+ if ( line.includes(exceptionToken) ) { continue; }
+ line = line.trim();
+ const match = safe.RegExp_exec.call(reLine, line);
+ if ( match === null ) { continue; }
+ let url = match[2];
+ if ( url.startsWith('(') ) { url = url.slice(1); }
+ if ( url === docURL.href ) {
+ url = 'inlineScript';
+ } else if ( url.startsWith('') ) {
+ url = 'injectedScript';
+ }
+ let fn = match[1] !== undefined
+ ? match[1].slice(0, -1)
+ : line.slice(0, match.index).trim();
+ if ( fn.startsWith('at') ) { fn = fn.slice(2).trim(); }
+ let rowcol = match[3];
+ lines.push(' ' + `${fn} ${url}${rowcol}:1`.trim());
+ }
+ lines[0] = `stackDepth:${lines.length-1}`;
+ const stack = lines.join('\t');
+ const r = needleDetails.matchAll !== true &&
+ safe.testPattern(needleDetails, stack);
+ if (
+ logLevel === 'all' ||
+ logLevel === 'match' && r ||
+ logLevel === 'nomatch' && !r
+ ) {
+ safe.uboLog(stack.replace(/\t/g, '\n'));
+ }
+ return r;
+}
+registerScriptlet(matchesStackTraceFn, {
+ name: 'matches-stack-trace.fn',
+ dependencies: [
+ getExceptionTokenFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+function abortOnStackTrace(
+ chain = '',
+ needle = '',
+ ...varargs
+) {
+ if ( typeof chain !== 'string' ) { return; }
+ const safe = safeSelf();
+ const needleDetails = safe.initPattern(needle, { canNegate: true });
+ const extraArgs = safe.parseVarargs(varargs);
+ if ( needle === '' ) { extraArgs.log = 'all'; }
+ const makeProxy = function(owner, chain) {
+ const pos = chain.indexOf('.');
+ if ( pos === -1 ) {
+ let v = owner[chain];
+ Object.defineProperty(owner, chain, {
+ get: function() {
+ const log = safe.logLevel > 1 ? 'all' : 'match';
+ if ( matchesStackTraceFn(needleDetails, log) ) {
+ throw new ReferenceError(getExceptionTokenFn());
+ }
+ return v;
+ },
+ set: function(a) {
+ const log = safe.logLevel > 1 ? 'all' : 'match';
+ if ( matchesStackTraceFn(needleDetails, log) ) {
+ throw new ReferenceError(getExceptionTokenFn());
+ }
+ v = a;
+ },
+ });
+ return;
+ }
+ const prop = chain.slice(0, pos);
+ let v = owner[prop];
+ chain = chain.slice(pos + 1);
+ if ( v ) {
+ makeProxy(v, chain);
+ return;
+ }
+ const desc = Object.getOwnPropertyDescriptor(owner, prop);
+ if ( desc && desc.set !== undefined ) { return; }
+ Object.defineProperty(owner, prop, {
+ get: function() { return v; },
+ set: function(a) {
+ v = a;
+ if ( a instanceof Object ) {
+ makeProxy(a, chain);
+ }
+ }
+ });
+ };
+ const owner = window;
+ makeProxy(owner, chain);
+}
+registerScriptlet(abortOnStackTrace, {
+ name: 'abort-on-stack-trace.js',
+ aliases: [
+ 'aost.js',
+ ],
+ dependencies: [
+ getExceptionTokenFn,
+ matchesStackTraceFn,
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/utils.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/utils.js
new file mode 100644
index 00000000..d492bd5e
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/utils.js
@@ -0,0 +1,445 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2019-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+/******************************************************************************/
+
+export function getRandomTokenFn() {
+ const safe = safeSelf();
+ return safe.String_fromCharCode(Date.now() % 26 + 97) +
+ safe.Math_floor(safe.Math_random() * 982451653 + 982451653).toString(36);
+}
+registerScriptlet(getRandomTokenFn, {
+ name: 'get-random-token.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+export function getExceptionTokenFn() {
+ const token = getRandomTokenFn();
+ const oe = self.onerror;
+ self.onerror = function(msg, ...args) {
+ if ( typeof msg === 'string' && msg.includes(token) ) { return true; }
+ if ( oe instanceof Function ) {
+ return oe.call(this, msg, ...args);
+ }
+ }.bind();
+ return token;
+}
+registerScriptlet(getExceptionTokenFn, {
+ name: 'get-exception-token.fn',
+ dependencies: [
+ getRandomTokenFn,
+ ],
+});
+
+/******************************************************************************/
+
+export function trapPropertyFn(propChain, handler, options = {}) {
+ if ( propChain === '' ) { return; }
+ let owner = self;
+ let prop = propChain;
+ for (;;) {
+ const pos = prop.indexOf('.');
+ if ( pos === -1 ) { break; }
+ owner = owner[prop.slice(0, pos)];
+ if ( owner instanceof Object === false ) { return; }
+ prop = prop.slice(pos + 1);
+ }
+ const safe = safeSelf();
+ if ( trapPropertyFn.db === undefined ) {
+ trapPropertyFn.db = new WeakMap();
+ trapPropertyFn.entryFromContext = (owner, prop) => {
+ const handlers = trapPropertyFn.db.get(owner);
+ return handlers?.get(prop);
+ };
+ trapPropertyFn.getter = (owner, prop) => {
+ const entry = trapPropertyFn.entryFromContext(owner, prop);
+ if ( entry === undefined ) { return; }
+ let r = entry.value;
+ for ( const desc of entry.stack ) {
+ try { r = desc.get(); } catch (e) {
+ if ( entry.canThrow ) { throw e; }
+ }
+ }
+ return r;
+ };
+ trapPropertyFn.setter = (owner, prop, value) => {
+ const entry = trapPropertyFn.entryFromContext(owner, prop);
+ if ( entry === undefined ) { return; }
+ entry.value = value;
+ for ( const desc of entry.stack ) {
+ try { desc.set(value); } catch (e) {
+ if ( entry.canThrow ) { throw e; }
+ }
+ }
+ };
+ }
+ const { db } = trapPropertyFn;
+ const handlers = db.get(owner) || new Map();
+ if ( handlers.size === 0 ) {
+ db.set(owner, handlers);
+ }
+ const entry = handlers.get(prop) || {
+ value: owner[prop],
+ stack: [],
+ };
+ entry.stack.push(handler);
+ if ( entry.stack.length > 1 ) { return entry.value; }
+ Object.assign(entry, options);
+ handlers.set(prop, entry);
+ const desc = safe.Object_getOwnPropertyDescriptor(owner, prop);
+ if ( desc instanceof safe.Object ) {
+ if ( desc.get || desc.set ) {
+ entry.stack.push(desc);
+ }
+ }
+ try {
+ safe.Object_defineProperty(owner, prop, {
+ get() {
+ return trapPropertyFn.getter(owner, prop);
+ },
+ set(value) {
+ trapPropertyFn.setter(owner, prop, value);
+ }
+ });
+ } catch {
+ }
+ return entry.value;
+}
+registerScriptlet(trapPropertyFn, {
+ name: 'trap-property-access.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+export function collateFetchArgumentsFn(resource, options) {
+ const safe = safeSelf();
+ const props = [
+ 'body', 'cache', 'credentials', 'duplex', 'headers',
+ 'integrity', 'keepalive', 'method', 'mode', 'priority',
+ 'redirect', 'referrer', 'referrerPolicy', 'url'
+ ];
+ const out = {};
+ if ( collateFetchArgumentsFn.collateKnownProps === undefined ) {
+ collateFetchArgumentsFn.collateKnownProps = (src, out) => {
+ for ( const prop of props ) {
+ if ( src[prop] === undefined ) { continue; }
+ out[prop] = src[prop];
+ }
+ };
+ }
+ if (
+ typeof resource !== 'object' ||
+ safe.Object_toString.call(resource) !== '[object Request]'
+ ) {
+ out.url = `${resource}`;
+ } else {
+ let clone;
+ try {
+ clone = safe.Request_clone.call(resource);
+ } catch {
+ }
+ collateFetchArgumentsFn.collateKnownProps(clone || resource, out);
+ }
+ if ( typeof options === 'object' && options !== null ) {
+ collateFetchArgumentsFn.collateKnownProps(options, out);
+ }
+ return out;
+}
+registerScriptlet(collateFetchArgumentsFn, {
+ name: 'collate-fetch-arguments.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+export function parsePropertiesToMatchFn(propsToMatch, implicit = '') {
+ const safe = safeSelf();
+ const needles = new Map();
+ if ( propsToMatch === undefined || propsToMatch === '' ) { return needles; }
+ const options = { canNegate: true };
+ for ( const needle of safe.String_split.call(propsToMatch, /\s+/) ) {
+ let [ prop, pattern ] = safe.String_split.call(needle, ':');
+ if ( prop === '' ) { continue; }
+ if ( pattern !== undefined && /[^$\w -]/.test(prop) ) {
+ prop = `${prop}:${pattern}`;
+ pattern = undefined;
+ }
+ if ( pattern !== undefined ) {
+ needles.set(prop, safe.initPattern(pattern, options));
+ } else if ( implicit !== '' ) {
+ needles.set(implicit, safe.initPattern(prop, options));
+ }
+ }
+ return needles;
+}
+registerScriptlet(parsePropertiesToMatchFn, {
+ name: 'parse-properties-to-match.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+export function matchObjectPropertiesFn(propNeedles, ...objs) {
+ const safe = safeSelf();
+ const matched = [];
+ for ( const obj of objs ) {
+ if ( obj instanceof Object === false ) { continue; }
+ for ( const [ prop, details ] of propNeedles ) {
+ let value = obj[prop];
+ if ( value === undefined ) { continue; }
+ if ( typeof value !== 'string' ) {
+ try { value = safe.JSON_stringify(value); }
+ catch { }
+ if ( typeof value !== 'string' ) { continue; }
+ }
+ if ( safe.testPattern(details, value) === false ) { return; }
+ matched.push(`${prop}: ${value}`);
+ }
+ }
+ return matched;
+}
+registerScriptlet(matchObjectPropertiesFn, {
+ name: 'match-object-properties.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+// Reference:
+// https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr
+//
+// Added `trusted` argument to allow for returning arbitrary text. Can only
+// be used through scriptlets requiring trusted source.
+
+export function generateContentFn(trusted, directive) {
+ const safe = safeSelf();
+ const randomize = len => {
+ const chunks = [];
+ let textSize = 0;
+ do {
+ const s = safe.Math_random().toString(36).slice(2);
+ chunks.push(s);
+ textSize += s.length;
+ }
+ while ( textSize < len );
+ return chunks.join(' ').slice(0, len);
+ };
+ if ( directive === 'true' ) {
+ return randomize(10);
+ }
+ if ( directive === 'emptyObj' ) {
+ return '{}';
+ }
+ if ( directive === 'emptyArr' ) {
+ return '[]';
+ }
+ if ( directive === 'emptyStr' ) {
+ return '';
+ }
+ if ( directive.startsWith('length:') ) {
+ const match = /^length:(\d+)(?:-(\d+))?$/.exec(directive);
+ if ( match === null ) { return ''; }
+ const min = parseInt(match[1], 10);
+ const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min;
+ const len = safe.Math_min(min + extent * safe.Math_random(), 500000);
+ return randomize(len | 0);
+ }
+ if ( directive.startsWith('war:') ) {
+ // TODO: resources should be fetched by messaging ISOLATED world
+ return '';
+ }
+ if ( directive.startsWith('join:') ) {
+ const parts = directive.slice(7)
+ .split(directive.slice(5, 7))
+ .map(a => generateContentFn(trusted, a));
+ return parts.some(a => a instanceof Promise)
+ ? Promise.all(parts).then(parts => parts.join(''))
+ : parts.join('');
+ }
+ if ( trusted ) {
+ return directive;
+ }
+ return '';
+}
+registerScriptlet(generateContentFn, {
+ name: 'generate-content.fn',
+ dependencies: [
+ safeSelf,
+ ],
+});
+
+/******************************************************************************/
+
+export function onIdleFn(fn, options) {
+ if ( self.requestIdleCallback ) {
+ return self.requestIdleCallback(fn, options);
+ }
+ return self.requestAnimationFrame(fn);
+}
+registerScriptlet(onIdleFn, {
+ name: 'on-idle.fn',
+});
+
+export function offIdleFn(id) {
+ if ( self.requestIdleCallback ) {
+ return self.cancelIdleCallback(id);
+ }
+ return self.cancelAnimationFrame(id);
+}
+registerScriptlet(offIdleFn, {
+ name: 'off-idle.fn',
+});
+
+/******************************************************************************/
+
+export function sleepFn(ms = 0) {
+ const nap = ( ) => {
+ return new Promise(resolve => {
+ self.requestAnimationFrame(resolve);
+ });
+ }
+ const until = Date.now() + ms;
+ const sleep = async resolve => {
+ do {
+ await nap();
+ } while ( Date.now() < until );
+ resolve();
+ };
+ return new Promise(resolve => { sleep(resolve); });
+}
+registerScriptlet(sleepFn, {
+ name: 'sleep.fn',
+});
+
+/******************************************************************************/
+
+export function lookupElementsFn(directive, until = 0) {
+ if ( lookupElementsFn.querySelectorEx === undefined ) {
+ lookupElementsFn.getShadowRoot = elem => {
+ if ( elem.openOrClosedShadowRoot ) { // Firefox
+ return elem.openOrClosedShadowRoot;
+ }
+ if ( self.chrome?.dom?.openOrClosedShadowRoot ) { // Chromium
+ return self.chrome.dom.openOrClosedShadowRoot(elem);
+ }
+ return elem.shadowRoot;
+ };
+ lookupElementsFn.queryOrEvaluateSelector = (selector, context) => {
+ if ( selector.startsWith('xpath:') === false ) {
+ return Array.from(context.querySelectorAll(selector));
+ }
+ const result = document.evaluate(selector.slice(6), context, null, 7, null);
+ const out = [];
+ if ( result.resultType === 7 ) {
+ for ( let i = 0; i < result.snapshotLength; i++ ) {
+ out[i] = result.snapshotItem(i);
+ }
+ }
+ return out;
+ }
+ lookupElementsFn.querySelectorEx = (selector, context = document) => {
+ const pos = selector.indexOf(' >>> ');
+ if ( pos === -1 ) {
+ return lookupElementsFn.queryOrEvaluateSelector(selector, context);
+ }
+ const outside = selector.slice(0, pos).trim();
+ const inside = selector.slice(pos + 5).trim();
+ const elems = lookupElementsFn.queryOrEvaluateSelector(outside, context);
+ const out = [];
+ for ( let i = 0; i < elems.length; i++ ) {
+ const shadowRoot = lookupElementsFn.getShadowRoot(elems[i]);
+ if ( Boolean(shadowRoot) === false ) { continue; }
+ lookupElementsFn.querySelectorEx(inside, shadowRoot).forEach(a => out.push(a));
+ }
+ return out;
+ };
+ lookupElementsFn.lookup = directive => {
+ const beVisible = directive.startsWith('when-visible:');
+ const selector = beVisible ? directive.slice(13) : directive;
+ const elems = lookupElementsFn.querySelectorEx(selector);
+ if ( beVisible !== true ) { return elems; }
+ return elems.filter(a => a.checkVisibility({
+ opacityProperty: true,
+ visibilityProperty: true,
+ }));
+ };
+ lookupElementsFn.lookupAsync = details => {
+ const elems = lookupElementsFn.lookup(details.directive);
+ if ( elems.length || Date.now() >= details.until ) {
+ if ( details.observer ) {
+ details.observer.disconnect();
+ details.observer = undefined;
+ }
+ if ( details.timer ) {
+ offIdleFn(details.timer);
+ details.timer = undefined;
+ }
+ return details.resolve(elems);
+ }
+ if ( details.observer === undefined ) {
+ details.observer = new MutationObserver(( ) => {
+ lookupElementsFn.lookupAsync(details);
+ });
+ details.observer.observe(document, {
+ attributes: true,
+ childList: true,
+ subtree: true,
+ });
+ }
+ if ( details.timer === undefined ) {
+ details.timer = onIdleFn(( ) => {
+ details.timer = undefined;
+ lookupElementsFn.lookupAsync(details);
+ }, { timeout: 151 });
+ }
+ };
+ }
+ if ( until === 0 ) {
+ return lookupElementsFn.lookup(directive);
+ }
+ return new Promise(resolve => {
+ lookupElementsFn.lookupAsync({ directive, until, resolve });
+ });
+}
+registerScriptlet(lookupElementsFn, {
+ name: 'lookup-elements.fn',
+ dependencies: [
+ offIdleFn,
+ onIdleFn,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlet-resources/vod.js b/privaxy/src/resources/vendor/ublock/scriptlet-resources/vod.js
new file mode 100644
index 00000000..f74d2595
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/scriptlet-resources/vod.js
@@ -0,0 +1,175 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2026-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+
+*/
+
+import { modifyXhrResponseFn } from './prevent-xhr.js';
+import { registerScriptlet } from './base.js';
+import { safeSelf } from './safe-self.js';
+
+export function mpegdashPrune(
+ selector = '',
+ propsToMatch = ''
+) {
+ if ( typeof selector !== 'string' ) { return; }
+ if ( selector === '' ) { return; }
+ const safe = safeSelf();
+ const logPrefix = safe.makeLogPrefix('mpegdash-prune', selector, propsToMatch);
+ const queryAll = (xmlDoc, selector) => {
+ if ( selector.startsWith('xpath:') === false ) {
+ return Array.from(xmlDoc.querySelectorAll(selector));
+ }
+ const xpr = xmlDoc.evaluate(
+ selector.slice(6),
+ xmlDoc,
+ null,
+ XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
+ null
+ );
+ const out = [];
+ for ( let i = 0; i < xpr.snapshotLength; i++ ) {
+ const node = xpr.snapshotItem(i);
+ out.push(node);
+ }
+ return out;
+ };
+ const rePTparse = /^PT(\d+D)?(\d+H)?(\d+M)?([\d.]+S)?$/;
+ const secondsPerDay = 24 * 60 * 60;
+ const secondsPerHour = 60 * 60;
+ const secondsPerMinute = 60;
+ const secondsFromPT = pt => {
+ const match = rePTparse.exec(pt);
+ if ( match === null ) { return; }
+ let seconds = 0;
+ if ( match[1] ) {
+ const d = parseFloat(match[1].slice(0, -1));
+ if ( isNaN(d) ) { return; }
+ seconds += d * secondsPerDay;
+ }
+ if ( match[2] ) {
+ const h = parseFloat(match[2].slice(0, -1));
+ if ( isNaN(h) ) { return; }
+ seconds += h * secondsPerHour;
+ }
+ if ( match[3] ) {
+ const m = parseFloat(match[3].slice(0, -1));
+ if ( isNaN(m) ) { return; }
+ seconds += m * secondsPerMinute;
+ }
+ if ( match[4] ) {
+ const s = parseFloat(match[4].slice(0, -1));
+ if ( isNaN(s) ) { return; }
+ seconds += s;
+ }
+ return seconds;
+ };
+ const ptFromSeconds = seconds => {
+ const parts = [ 'PT' ];
+ const d = Math.floor(seconds / secondsPerDay);
+ if ( d ) {
+ parts.push(`${d}D`);
+ seconds -= d * secondsPerDay;
+ }
+ const h = Math.floor(seconds / secondsPerHour);
+ if ( h ) {
+ parts.push(`${h}H`);
+ seconds -= h * secondsPerHour;
+ }
+ const m = Math.floor(seconds / secondsPerMinute);
+ if ( m ) {
+ parts.push(`${m}M`);
+ seconds -= m * secondsPerMinute;
+ }
+ parts.push(`${seconds}S`);
+ return parts.join('');
+ };
+ const fixTimeAttributes = xmlDoc => {
+ try {
+ const periods = queryAll(xmlDoc, 'MPD > Period');
+ if ( periods.length === 0 ) { return; }
+ let seconds = 0;
+ for ( const period of periods ) {
+ const startAttrBefore = period.getAttribute('start');
+ const durAttr = period.getAttribute('duration');
+ if ( startAttrBefore === null || durAttr === null ) { continue; }
+ const startAttrAfter = ptFromSeconds(seconds);
+ period.setAttribute('start', startAttrAfter);
+ if ( period.hasAttribute('id') ) {
+ const idAttr = period.getAttribute('id');
+ period.setAttribute('id', idAttr.replace(startAttrBefore, startAttrAfter));
+ }
+ seconds += secondsFromPT(durAttr);
+ }
+ const mpds = queryAll(xmlDoc, 'MPD[mediaPresentationDuration]');
+ if ( mpds.length !== 1 ) { return; }
+ mpds[0].setAttribute('mediaPresentationDuration', ptFromSeconds(seconds));
+ } catch {
+ }
+ };
+ const pruneFromDoc = xmlDoc => {
+ try {
+ if ( selector === '' ) {
+ const serializer = new XMLSerializer();
+ safe.uboLog(logPrefix, `Document is\n\t${serializer.serializeToString(xmlDoc)}`);
+ }
+ const items = queryAll(xmlDoc, selector);
+ if ( items.length === 0 ) { return xmlDoc; }
+ safe.uboLog(logPrefix, `Patching ${items.length} items`);
+ for ( const item of items ) {
+ if ( item.nodeType !== 1 ) { continue; }
+ item.setAttribute('duration', 'PT0S');
+ }
+ fixTimeAttributes(xmlDoc);
+ } catch(ex) {
+ safe.uboErr(logPrefix, `Error: ${ex}`);
+ }
+ return xmlDoc;
+ };
+ const pruneFromText = text => {
+ if ( (/^\s*\s*$/.test(text)) === false ) {
+ return text;
+ }
+ try {
+ const xmlParser = new DOMParser();
+ const xmlDoc = xmlParser.parseFromString(text, 'text/xml');
+ pruneFromDoc(xmlDoc);
+ const serializer = new XMLSerializer();
+ text = serializer.serializeToString(xmlDoc);
+ } catch {
+ }
+ return text;
+ };
+ modifyXhrResponseFn(propsToMatch, (xhr, before) => {
+ if ( before instanceof XMLDocument ) {
+ return pruneFromDoc(before);
+ }
+ if ( typeof before === 'string' ) {
+ return pruneFromText(before);
+ }
+ return before;
+ });
+}
+registerScriptlet(mpegdashPrune, {
+ name: 'mpegdash-prune.js',
+ dependencies: [
+ modifyXhrResponseFn,
+ safeSelf,
+ ],
+});
diff --git a/privaxy/src/resources/vendor/ublock/scriptlets.js b/privaxy/src/resources/vendor/ublock/scriptlets.js
index f214a9f8..5d3b579d 100644
--- a/privaxy/src/resources/vendor/ublock/scriptlets.js
+++ b/privaxy/src/resources/vendor/ublock/scriptlets.js
@@ -1,4802 +1,2 @@
-/*******************************************************************************
-
- uBlock Origin - a comprehensive, efficient content blocker
- Copyright (C) 2019-present Raymond Hill
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see {http://www.gnu.org/licenses/}.
-
- Home: https://github.com/gorhill/uBlock
-
- The scriptlets below are meant to be injected only into a
- web page context.
-*/
-
-/* eslint no-prototype-builtins: 0 */
-
-// Externally added to the private namespace in which scriptlets execute.
-/* global scriptletGlobals */
-
-export const builtinScriptlets = [];
-
-/*******************************************************************************
-
- Helper functions
-
- These are meant to be used as dependencies to injectable scriptlets.
-
-*******************************************************************************/
-
-builtinScriptlets.push({
- name: 'safe-self.fn',
- fn: safeSelf,
-});
-function safeSelf() {
- if ( scriptletGlobals.safeSelf ) {
- return scriptletGlobals.safeSelf;
- }
- const self = globalThis;
- const safe = {
- 'Array_from': Array.from,
- 'Error': self.Error,
- 'Function_toStringFn': self.Function.prototype.toString,
- 'Function_toString': thisArg => safe.Function_toStringFn.call(thisArg),
- 'Math_floor': Math.floor,
- 'Math_max': Math.max,
- 'Math_min': Math.min,
- 'Math_random': Math.random,
- 'Object': Object,
- 'Object_defineProperty': Object.defineProperty.bind(Object),
- 'Object_fromEntries': Object.fromEntries.bind(Object),
- 'Object_getOwnPropertyDescriptor': Object.getOwnPropertyDescriptor.bind(Object),
- 'RegExp': self.RegExp,
- 'RegExp_test': self.RegExp.prototype.test,
- 'RegExp_exec': self.RegExp.prototype.exec,
- 'Request_clone': self.Request.prototype.clone,
- 'XMLHttpRequest': self.XMLHttpRequest,
- 'addEventListener': self.EventTarget.prototype.addEventListener,
- 'removeEventListener': self.EventTarget.prototype.removeEventListener,
- 'fetch': self.fetch,
- 'JSON': self.JSON,
- 'JSON_parseFn': self.JSON.parse,
- 'JSON_stringifyFn': self.JSON.stringify,
- 'JSON_parse': (...args) => safe.JSON_parseFn.call(safe.JSON, ...args),
- 'JSON_stringify': (...args) => safe.JSON_stringifyFn.call(safe.JSON, ...args),
- 'log': console.log.bind(console),
- // Properties
- logLevel: 0,
- // Methods
- makeLogPrefix(...args) {
- return this.sendToLogger && `[${args.join(' \u205D ')}]` || '';
- },
- uboLog(...args) {
- if ( this.sendToLogger === undefined ) { return; }
- if ( args === undefined || args[0] === '' ) { return; }
- return this.sendToLogger('info', ...args);
-
- },
- uboErr(...args) {
- if ( this.sendToLogger === undefined ) { return; }
- if ( args === undefined || args[0] === '' ) { return; }
- return this.sendToLogger('error', ...args);
- },
- escapeRegexChars(s) {
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
- },
- initPattern(pattern, options = {}) {
- if ( pattern === '' ) {
- return { matchAll: true };
- }
- const expect = (options.canNegate !== true || pattern.startsWith('!') === false);
- if ( expect === false ) {
- pattern = pattern.slice(1);
- }
- const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
- if ( match !== null ) {
- return {
- re: new this.RegExp(
- match[1],
- match[2] || options.flags
- ),
- expect,
- };
- }
- if ( options.flags !== undefined ) {
- return {
- re: new this.RegExp(this.escapeRegexChars(pattern),
- options.flags
- ),
- expect,
- };
- }
- return { pattern, expect };
- },
- testPattern(details, haystack) {
- if ( details.matchAll ) { return true; }
- if ( details.re ) {
- return this.RegExp_test.call(details.re, haystack) === details.expect;
- }
- return haystack.includes(details.pattern) === details.expect;
- },
- patternToRegex(pattern, flags = undefined, verbatim = false) {
- if ( pattern === '' ) { return /^/; }
- const match = /^\/(.+)\/([gimsu]*)$/.exec(pattern);
- if ( match === null ) {
- const reStr = this.escapeRegexChars(pattern);
- return new RegExp(verbatim ? `^${reStr}$` : reStr, flags);
- }
- try {
- return new RegExp(match[1], match[2] || undefined);
- }
- catch(ex) {
- }
- return /^/;
- },
- getExtraArgs(args, offset = 0) {
- const entries = args.slice(offset).reduce((out, v, i, a) => {
- if ( (i & 1) === 0 ) {
- const rawValue = a[i+1];
- const value = /^\d+$/.test(rawValue)
- ? parseInt(rawValue, 10)
- : rawValue;
- out.push([ a[i], value ]);
- }
- return out;
- }, []);
- return this.Object_fromEntries(entries);
- },
- onIdle(fn, options) {
- if ( self.requestIdleCallback ) {
- return self.requestIdleCallback(fn, options);
- }
- return self.requestAnimationFrame(fn);
- },
- };
- scriptletGlobals.safeSelf = safe;
- if ( scriptletGlobals.bcSecret === undefined ) { return safe; }
- // This is executed only when the logger is opened
- const bc = new self.BroadcastChannel(scriptletGlobals.bcSecret);
- let bcBuffer = [];
- safe.logLevel = scriptletGlobals.logLevel || 1;
- safe.sendToLogger = (type, ...args) => {
- if ( args.length === 0 ) { return; }
- const text = `[${document.location.hostname || document.location.href}]${args.join(' ')}`;
- if ( bcBuffer === undefined ) {
- return bc.postMessage({ what: 'messageToLogger', type, text });
- }
- bcBuffer.push({ type, text });
- };
- bc.onmessage = ev => {
- const msg = ev.data;
- switch ( msg ) {
- case 'iamready!':
- if ( bcBuffer === undefined ) { break; }
- bcBuffer.forEach(({ type, text }) =>
- bc.postMessage({ what: 'messageToLogger', type, text })
- );
- bcBuffer = undefined;
- break;
- case 'setScriptletLogLevelToOne':
- safe.logLevel = 1;
- break;
- case 'setScriptletLogLevelToTwo':
- safe.logLevel = 2;
- break;
- }
- };
- bc.postMessage('areyouready?');
- return safe;
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'get-exception-token.fn',
- fn: getExceptionToken,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function getExceptionToken() {
- const safe = safeSelf();
- const token =
- String.fromCharCode(Date.now() % 26 + 97) +
- safe.Math_floor(safe.Math_random() * 982451653 + 982451653).toString(36);
- const oe = self.onerror;
- self.onerror = function(msg, ...args) {
- if ( typeof msg === 'string' && msg.includes(token) ) { return true; }
- if ( oe instanceof Function ) {
- return oe.call(this, msg, ...args);
- }
- }.bind();
- return token;
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'should-debug.fn',
- fn: shouldDebug,
-});
-function shouldDebug(details) {
- if ( details instanceof Object === false ) { return false; }
- return scriptletGlobals.canDebug && details.debug;
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'run-at.fn',
- fn: runAt,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function runAt(fn, when) {
- const intFromReadyState = state => {
- const targets = {
- 'loading': 1,
- 'interactive': 2, 'end': 2, '2': 2,
- 'complete': 3, 'idle': 3, '3': 3,
- };
- const tokens = Array.isArray(state) ? state : [ state ];
- for ( const token of tokens ) {
- const prop = `${token}`;
- if ( targets.hasOwnProperty(prop) === false ) { continue; }
- return targets[prop];
- }
- return 0;
- };
- const runAt = intFromReadyState(when);
- if ( intFromReadyState(document.readyState) >= runAt ) {
- fn(); return;
- }
- const onStateChange = ( ) => {
- if ( intFromReadyState(document.readyState) < runAt ) { return; }
- fn();
- safe.removeEventListener.apply(document, args);
- };
- const safe = safeSelf();
- const args = [ 'readystatechange', onStateChange, { capture: true } ];
- safe.addEventListener.apply(document, args);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'run-at-html-element.fn',
- fn: runAtHtmlElementFn,
-});
-function runAtHtmlElementFn(fn) {
- if ( document.documentElement ) {
- fn();
- return;
- }
- const observer = new MutationObserver(( ) => {
- observer.disconnect();
- fn();
- });
- observer.observe(document, { childList: true });
-}
-
-/******************************************************************************/
-
-// Reference:
-// https://github.com/AdguardTeam/Scriptlets/blob/master/wiki/about-scriptlets.md#prevent-xhr
-
-builtinScriptlets.push({
- name: 'generate-content.fn',
- fn: generateContentFn,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function generateContentFn(directive) {
- const safe = safeSelf();
- const randomize = len => {
- const chunks = [];
- let textSize = 0;
- do {
- const s = safe.Math_random().toString(36).slice(2);
- chunks.push(s);
- textSize += s.length;
- }
- while ( textSize < len );
- return chunks.join(' ').slice(0, len);
- };
- if ( directive === 'true' ) {
- return Promise.resolve(randomize(10));
- }
- if ( directive === 'emptyObj' ) {
- return Promise.resolve('{}');
- }
- if ( directive === 'emptyArr' ) {
- return Promise.resolve('[]');
- }
- if ( directive === 'emptyStr' ) {
- return Promise.resolve('');
- }
- if ( directive.startsWith('length:') ) {
- const match = /^length:(\d+)(?:-(\d+))?$/.exec(directive);
- if ( match ) {
- const min = parseInt(match[1], 10);
- const extent = safe.Math_max(parseInt(match[2], 10) || 0, min) - min;
- const len = safe.Math_min(min + extent * safe.Math_random(), 500000);
- return Promise.resolve(randomize(len | 0));
- }
- }
- if ( directive.startsWith('war:') && scriptletGlobals.warOrigin ) {
- return new Promise(resolve => {
- const warOrigin = scriptletGlobals.warOrigin;
- const warName = directive.slice(4);
- const fullpath = [ warOrigin, '/', warName ];
- const warSecret = scriptletGlobals.warSecret;
- if ( warSecret !== undefined ) {
- fullpath.push('?secret=', warSecret);
- }
- const warXHR = new safe.XMLHttpRequest();
- warXHR.responseType = 'text';
- warXHR.onloadend = ev => {
- resolve(ev.target.responseText || '');
- };
- warXHR.open('GET', fullpath.join(''));
- warXHR.send();
- });
- }
- return Promise.resolve('');
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'abort-current-script-core.fn',
- fn: abortCurrentScriptCore,
- dependencies: [
- 'get-exception-token.fn',
- 'safe-self.fn',
- 'should-debug.fn',
- ],
-});
-// Issues to mind before changing anything:
-// https://github.com/uBlockOrigin/uBlock-issues/issues/2154
-function abortCurrentScriptCore(
- target = '',
- needle = '',
- context = ''
-) {
- if ( typeof target !== 'string' ) { return; }
- if ( target === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('abort-current-script', target, needle, context);
- const reNeedle = safe.patternToRegex(needle);
- const reContext = safe.patternToRegex(context);
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
- const thisScript = document.currentScript;
- const chain = target.split('.');
- let owner = window;
- let prop;
- for (;;) {
- prop = chain.shift();
- if ( chain.length === 0 ) { break; }
- if ( prop in owner === false ) { break; }
- owner = owner[prop];
- if ( owner instanceof Object === false ) { return; }
- }
- let value;
- let desc = Object.getOwnPropertyDescriptor(owner, prop);
- if (
- desc instanceof Object === false ||
- desc.get instanceof Function === false
- ) {
- value = owner[prop];
- desc = undefined;
- }
- const debug = shouldDebug(extraArgs);
- const exceptionToken = getExceptionToken();
- const scriptTexts = new WeakMap();
- const getScriptText = elem => {
- let text = elem.textContent;
- if ( text.trim() !== '' ) { return text; }
- if ( scriptTexts.has(elem) ) { return scriptTexts.get(elem); }
- const [ , mime, content ] =
- /^data:([^,]*),(.+)$/.exec(elem.src.trim()) ||
- [ '', '', '' ];
- try {
- switch ( true ) {
- case mime.endsWith(';base64'):
- text = self.atob(content);
- break;
- default:
- text = self.decodeURIComponent(content);
- break;
- }
- } catch(ex) {
- }
- scriptTexts.set(elem, text);
- return text;
- };
- const validate = ( ) => {
- const e = document.currentScript;
- if ( e instanceof HTMLScriptElement === false ) { return; }
- if ( e === thisScript ) { return; }
- if ( context !== '' && reContext.test(e.src) === false ) {
- // eslint-disable-next-line no-debugger
- if ( debug === 'nomatch' || debug === 'all' ) { debugger; }
- return;
- }
- if ( safe.logLevel > 1 && context !== '' ) {
- safe.uboLog(logPrefix, `Matched src\n${e.src}`);
- }
- const scriptText = getScriptText(e);
- if ( reNeedle.test(scriptText) === false ) {
- // eslint-disable-next-line no-debugger
- if ( debug === 'nomatch' || debug === 'all' ) { debugger; }
- return;
- }
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Matched text\n${scriptText}`);
- }
- // eslint-disable-next-line no-debugger
- if ( debug === 'match' || debug === 'all' ) { debugger; }
- safe.uboLog(logPrefix, 'Aborted');
- throw new ReferenceError(exceptionToken);
- };
- // eslint-disable-next-line no-debugger
- if ( debug === 'install' ) { debugger; }
- try {
- Object.defineProperty(owner, prop, {
- get: function() {
- validate();
- return desc instanceof Object
- ? desc.get.call(owner)
- : value;
- },
- set: function(a) {
- validate();
- if ( desc instanceof Object ) {
- desc.set.call(owner, a);
- } else {
- value = a;
- }
- }
- });
- } catch(ex) {
- safe.uboErr(logPrefix, `Error: ${ex}`);
- }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'validate-constant.fn',
- fn: validateConstantFn,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function validateConstantFn(trusted, raw) {
- const safe = safeSelf();
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 2);
- let value;
- if ( raw === 'undefined' ) {
- value = undefined;
- } else if ( raw === 'false' ) {
- value = false;
- } else if ( raw === 'true' ) {
- value = true;
- } else if ( raw === 'null' ) {
- value = null;
- } else if ( raw === "''" || raw === '' ) {
- value = '';
- } else if ( raw === '[]' || raw === 'emptyArr' ) {
- value = [];
- } else if ( raw === '{}' || raw === 'emptyObj' ) {
- value = {};
- } else if ( raw === 'noopFunc' ) {
- value = function(){};
- } else if ( raw === 'trueFunc' ) {
- value = function(){ return true; };
- } else if ( raw === 'falseFunc' ) {
- value = function(){ return false; };
- } else if ( /^-?\d+$/.test(raw) ) {
- value = parseInt(raw);
- if ( isNaN(raw) ) { return; }
- if ( Math.abs(raw) > 0x7FFF ) { return; }
- } else if ( trusted ) {
- if ( raw.startsWith('{') && raw.endsWith('}') ) {
- try { value = safe.JSON_parse(raw).value; } catch(ex) { return; }
- }
- } else {
- return;
- }
- if ( extraArgs.as !== undefined ) {
- if ( extraArgs.as === 'function' ) {
- return ( ) => value;
- } else if ( extraArgs.as === 'callback' ) {
- return ( ) => (( ) => value);
- } else if ( extraArgs.as === 'resolved' ) {
- return Promise.resolve(value);
- } else if ( extraArgs.as === 'rejected' ) {
- return Promise.reject(value);
- }
- }
- return value;
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'set-constant.fn',
- fn: setConstantFn,
- dependencies: [
- 'run-at.fn',
- 'safe-self.fn',
- 'validate-constant.fn',
- ],
-});
-function setConstantFn(
- trusted = false,
- chain = '',
- rawValue = ''
-) {
- if ( chain === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('set-constant', chain, rawValue);
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
- function setConstant(chain, rawValue) {
- const trappedProp = (( ) => {
- const pos = chain.lastIndexOf('.');
- if ( pos === -1 ) { return chain; }
- return chain.slice(pos+1);
- })();
- const cloakFunc = fn => {
- safe.Object_defineProperty(fn, 'name', { value: trappedProp });
- return new Proxy(fn, {
- defineProperty(target, prop) {
- if ( prop !== 'toString' ) {
- return Reflect.defineProperty(...arguments);
- }
- return true;
- },
- deleteProperty(target, prop) {
- if ( prop !== 'toString' ) {
- return Reflect.deleteProperty(...arguments);
- }
- return true;
- },
- get(target, prop) {
- if ( prop === 'toString' ) {
- return function() {
- return `function ${trappedProp}() { [native code] }`;
- }.bind(null);
- }
- return Reflect.get(...arguments);
- },
- });
- };
- if ( trappedProp === '' ) { return; }
- const thisScript = document.currentScript;
- let normalValue = validateConstantFn(trusted, rawValue);
- if ( rawValue === 'noopFunc' || rawValue === 'trueFunc' || rawValue === 'falseFunc' ) {
- normalValue = cloakFunc(normalValue);
- }
- let aborted = false;
- const mustAbort = function(v) {
- if ( trusted ) { return false; }
- if ( aborted ) { return true; }
- aborted =
- (v !== undefined && v !== null) &&
- (normalValue !== undefined && normalValue !== null) &&
- (typeof v !== typeof normalValue);
- if ( aborted ) {
- safe.uboLog(logPrefix, `Aborted because value set to ${v}`);
- }
- return aborted;
- };
- // https://github.com/uBlockOrigin/uBlock-issues/issues/156
- // Support multiple trappers for the same property.
- const trapProp = function(owner, prop, configurable, handler) {
- if ( handler.init(configurable ? owner[prop] : normalValue) === false ) { return; }
- const odesc = safe.Object_getOwnPropertyDescriptor(owner, prop);
- let prevGetter, prevSetter;
- if ( odesc instanceof safe.Object ) {
- owner[prop] = normalValue;
- if ( odesc.get instanceof Function ) {
- prevGetter = odesc.get;
- }
- if ( odesc.set instanceof Function ) {
- prevSetter = odesc.set;
- }
- }
- try {
- safe.Object_defineProperty(owner, prop, {
- configurable,
- get() {
- if ( prevGetter !== undefined ) {
- prevGetter();
- }
- return handler.getter();
- },
- set(a) {
- if ( prevSetter !== undefined ) {
- prevSetter(a);
- }
- handler.setter(a);
- }
- });
- safe.uboLog(logPrefix, 'Trap installed');
- } catch(ex) {
- safe.uboErr(logPrefix, ex);
- }
- };
- const trapChain = function(owner, chain) {
- const pos = chain.indexOf('.');
- if ( pos === -1 ) {
- trapProp(owner, chain, false, {
- v: undefined,
- init: function(v) {
- if ( mustAbort(v) ) { return false; }
- this.v = v;
- return true;
- },
- getter: function() {
- if ( document.currentScript === thisScript ) {
- return this.v;
- }
- safe.uboLog(logPrefix, 'Property read');
- return normalValue;
- },
- setter: function(a) {
- if ( mustAbort(a) === false ) { return; }
- normalValue = a;
- }
- });
- return;
- }
- const prop = chain.slice(0, pos);
- const v = owner[prop];
- chain = chain.slice(pos + 1);
- if ( v instanceof safe.Object || typeof v === 'object' && v !== null ) {
- trapChain(v, chain);
- return;
- }
- trapProp(owner, prop, true, {
- v: undefined,
- init: function(v) {
- this.v = v;
- return true;
- },
- getter: function() {
- return this.v;
- },
- setter: function(a) {
- this.v = a;
- if ( a instanceof safe.Object ) {
- trapChain(a, chain);
- }
- }
- });
- };
- trapChain(window, chain);
- }
- runAt(( ) => {
- setConstant(chain, rawValue);
- }, extraArgs.runAt);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'replace-node-text.fn',
- fn: replaceNodeTextFn,
- dependencies: [
- 'run-at.fn',
- 'safe-self.fn',
- ],
-});
-function replaceNodeTextFn(
- nodeName = '',
- pattern = '',
- replacement = ''
-) {
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('replace-node-text.fn', ...Array.from(arguments));
- const reNodeName = safe.patternToRegex(nodeName, 'i', true);
- const rePattern = safe.patternToRegex(pattern, 'gms');
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
- const reCondition = safe.patternToRegex(extraArgs.condition || '', 'ms');
- const stop = (takeRecord = true) => {
- if ( takeRecord ) {
- handleMutations(observer.takeRecords());
- }
- observer.disconnect();
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, 'Quitting');
- }
- };
- let sedCount = extraArgs.sedCount || 0;
- const handleNode = node => {
- const before = node.textContent;
- reCondition.lastIndex = 0;
- if ( safe.RegExp_test.call(reCondition, before) === false ) { return true; }
- rePattern.lastIndex = 0;
- if ( safe.RegExp_test.call(rePattern, before) === false ) { return true; }
- rePattern.lastIndex = 0;
- const after = pattern !== ''
- ? before.replace(rePattern, replacement)
- : replacement;
- node.textContent = after;
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Text before:\n${before.trim()}`);
- }
- safe.uboLog(logPrefix, `Text after:\n${after.trim()}`);
- return sedCount === 0 || (sedCount -= 1) !== 0;
- };
- const handleMutations = mutations => {
- for ( const mutation of mutations ) {
- for ( const node of mutation.addedNodes ) {
- if ( reNodeName.test(node.nodeName) === false ) { continue; }
- if ( handleNode(node) ) { continue; }
- stop(false); return;
- }
- }
- };
- const observer = new MutationObserver(handleMutations);
- observer.observe(document, { childList: true, subtree: true });
- if ( document.documentElement ) {
- const treeWalker = document.createTreeWalker(
- document.documentElement,
- NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
- );
- let count = 0;
- for (;;) {
- const node = treeWalker.nextNode();
- count += 1;
- if ( node === null ) { break; }
- if ( reNodeName.test(node.nodeName) === false ) { continue; }
- if ( handleNode(node) ) { continue; }
- stop(); break;
- }
- safe.uboLog(logPrefix, `${count} nodes present before installing mutation observer`);
- }
- if ( extraArgs.stay ) { return; }
- runAt(( ) => {
- const quitAfter = extraArgs.quitAfter || 0;
- if ( quitAfter !== 0 ) {
- setTimeout(( ) => { stop(); }, quitAfter);
- } else {
- stop();
- }
- }, 'interactive');
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'object-prune.fn',
- fn: objectPruneFn,
- dependencies: [
- 'matches-stack-trace.fn',
- 'object-find-owner.fn',
- ],
-});
-// When no "prune paths" argument is provided, the scriptlet is
-// used for logging purpose and the "needle paths" argument is
-// used to filter logging output.
-//
-// https://github.com/uBlockOrigin/uBlock-issues/issues/1545
-// - Add support for "remove everything if needle matches" case
-function objectPruneFn(
- obj,
- rawPrunePaths,
- rawNeedlePaths,
- stackNeedleDetails = { matchAll: true },
- extraArgs = {}
-) {
- if ( typeof rawPrunePaths !== 'string' ) { return; }
- const prunePaths = rawPrunePaths !== ''
- ? rawPrunePaths.split(/ +/)
- : [];
- const needlePaths = prunePaths.length !== 0 && rawNeedlePaths !== ''
- ? rawNeedlePaths.split(/ +/)
- : [];
- if ( stackNeedleDetails.matchAll !== true ) {
- if ( matchesStackTrace(stackNeedleDetails, extraArgs.logstack) === false ) {
- return;
- }
- }
- if ( objectPruneFn.mustProcess === undefined ) {
- objectPruneFn.mustProcess = (root, needlePaths) => {
- for ( const needlePath of needlePaths ) {
- if ( objectFindOwnerFn(root, needlePath) === false ) {
- return false;
- }
- }
- return true;
- };
- }
- if ( prunePaths.length === 0 ) { return; }
- let outcome = 'nomatch';
- if ( objectPruneFn.mustProcess(obj, needlePaths) ) {
- for ( const path of prunePaths ) {
- if ( objectFindOwnerFn(obj, path, true) ) {
- outcome = 'match';
- }
- }
- }
- if ( outcome === 'match' ) { return obj; }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'object-find-owner.fn',
- fn: objectFindOwnerFn,
-});
-function objectFindOwnerFn(
- root,
- path,
- prune = false
-) {
- let owner = root;
- let chain = path;
- for (;;) {
- if ( typeof owner !== 'object' || owner === null ) { return false; }
- const pos = chain.indexOf('.');
- if ( pos === -1 ) {
- if ( prune === false ) {
- return owner.hasOwnProperty(chain);
- }
- let modified = false;
- if ( chain === '*' ) {
- for ( const key in owner ) {
- if ( owner.hasOwnProperty(key) === false ) { continue; }
- delete owner[key];
- modified = true;
- }
- } else if ( owner.hasOwnProperty(chain) ) {
- delete owner[chain];
- modified = true;
- }
- return modified;
- }
- const prop = chain.slice(0, pos);
- const next = chain.slice(pos + 1);
- let found = false;
- if ( prop === '[-]' && Array.isArray(owner) ) {
- let i = owner.length;
- while ( i-- ) {
- if ( objectFindOwnerFn(owner[i], next) === false ) { continue; }
- owner.splice(i, 1);
- found = true;
- }
- return found;
- }
- if ( prop === '{-}' && owner instanceof Object ) {
- for ( const key of Object.keys(owner) ) {
- if ( objectFindOwnerFn(owner[key], next) === false ) { continue; }
- delete owner[key];
- found = true;
- }
- return found;
- }
- if (
- prop === '[]' && Array.isArray(owner) ||
- prop === '{}' && owner instanceof Object ||
- prop === '*' && owner instanceof Object
- ) {
- for ( const key of Object.keys(owner) ) {
- if (objectFindOwnerFn(owner[key], next, prune) === false ) { continue; }
- found = true;
- }
- return found;
- }
- if ( owner.hasOwnProperty(prop) === false ) { return false; }
- owner = owner[prop];
- chain = chain.slice(pos + 1);
- }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'get-all-cookies.fn',
- fn: getAllCookiesFn,
-});
-function getAllCookiesFn() {
- return document.cookie.split(/\s*;\s*/).map(s => {
- const pos = s.indexOf('=');
- if ( pos === 0 ) { return; }
- if ( pos === -1 ) { return `${s.trim()}=`; }
- const key = s.slice(0, pos).trim();
- const value = s.slice(pos+1).trim();
- return { key, value };
- }).filter(s => s !== undefined);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'get-all-local-storage.fn',
- fn: getAllLocalStorageFn,
-});
-function getAllLocalStorageFn(which = 'localStorage') {
- const storage = self[which];
- const out = [];
- for ( let i = 0; i < storage.length; i++ ) {
- const key = storage.key(i);
- const value = storage.getItem(key);
- return { key, value };
- }
- return out;
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'get-cookie.fn',
- fn: getCookieFn,
-});
-function getCookieFn(
- name = ''
-) {
- for ( const s of document.cookie.split(/\s*;\s*/) ) {
- const pos = s.indexOf('=');
- if ( pos === -1 ) { continue; }
- if ( s.slice(0, pos) !== name ) { continue; }
- return s.slice(pos+1).trim();
- }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'set-cookie.fn',
- fn: setCookieFn,
- dependencies: [
- 'get-cookie.fn',
- ],
-});
-function setCookieFn(
- trusted = false,
- name = '',
- value = '',
- expires = '',
- path = '',
- options = {},
-) {
- // https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
- // https://github.com/uBlockOrigin/uBlock-issues/issues/2777
- if ( trusted === false && /[^!#$%&'*+\-.0-9A-Z[\]^_`a-z|~]/.test(name) ) {
- name = encodeURIComponent(name);
- }
- // https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
- // The characters [",] are given a pass from the RFC requirements because
- // apparently browsers do not follow the RFC to the letter.
- if ( /[^ -:<-[\]-~]/.test(value) ) {
- value = encodeURIComponent(value);
- }
-
- const cookieBefore = getCookieFn(name);
- if ( cookieBefore !== undefined && options.dontOverwrite ) { return; }
- if ( cookieBefore === value && options.reload ) { return; }
-
- const cookieParts = [ name, '=', value ];
- if ( expires !== '' ) {
- cookieParts.push('; expires=', expires);
- }
-
- if ( path === '' ) { path = '/'; }
- else if ( path === 'none' ) { path = ''; }
- if ( path !== '' && path !== '/' ) { return; }
- if ( path === '/' ) {
- cookieParts.push('; path=/');
- }
-
- if ( trusted ) {
- if ( options.domain ) {
- cookieParts.push(`; domain=${options.domain}`);
- }
- cookieParts.push('; Secure');
- }
-
- try {
- document.cookie = cookieParts.join('');
- } catch(_) {
- }
-
- const done = getCookieFn(name) === value;
- if ( done && options.reload ) {
- window.location.reload();
- }
-
- return done;
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'set-local-storage-item.fn',
- fn: setLocalStorageItemFn,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function setLocalStorageItemFn(
- which = 'local',
- trusted = false,
- key = '',
- value = '',
-) {
- if ( key === '' ) { return; }
-
- // For increased compatibility with AdGuard
- if ( value === 'emptyArr' ) {
- value = '[]';
- } else if ( value === 'emptyObj' ) {
- value = '{}';
- }
-
- const trustedValues = [
- '',
- 'undefined', 'null',
- 'false', 'true',
- 'on', 'off',
- 'yes', 'no',
- 'accept', 'reject',
- 'accepted', 'rejected',
- '{}', '[]', '""',
- '$remove$',
- ];
-
- if ( trusted ) {
- if ( value.includes('$now$') ) {
- value = value.replaceAll('$now$', Date.now());
- }
- if ( value.includes('$currentDate$') ) {
- value = value.replaceAll('$currentDate$', `${Date()}`);
- }
- if ( value.includes('$currentISODate$') ) {
- value = value.replaceAll('$currentISODate$', (new Date()).toISOString());
- }
- } else {
- const normalized = value.toLowerCase();
- const match = /^("?)(.+)\1$/.exec(normalized);
- const unquoted = match && match[2] || normalized;
- if ( trustedValues.includes(unquoted) === false ) {
- if ( /^\d+$/.test(unquoted) === false ) { return; }
- const n = parseInt(unquoted, 10);
- if ( n > 32767 ) { return; }
- }
- }
-
- try {
- const storage = self[`${which}Storage`];
- if ( value === '$remove$' ) {
- const safe = safeSelf();
- const pattern = safe.patternToRegex(key, undefined, true );
- const toRemove = [];
- for ( let i = 0, n = storage.length; i < n; i++ ) {
- const key = storage.key(i);
- if ( pattern.test(key) ) { toRemove.push(key); }
- }
- for ( const key of toRemove ) {
- storage.removeItem(key);
- }
- } else {
- storage.setItem(key, `${value}`);
- }
- } catch(ex) {
- }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'matches-stack-trace.fn',
- fn: matchesStackTrace,
- dependencies: [
- 'get-exception-token.fn',
- 'safe-self.fn',
- ],
-});
-function matchesStackTrace(
- needleDetails,
- logLevel = ''
-) {
- const safe = safeSelf();
- const exceptionToken = getExceptionToken();
- const error = new safe.Error(exceptionToken);
- const docURL = new URL(self.location.href);
- docURL.hash = '';
- // Normalize stack trace
- const reLine = /(.*?@)?(\S+)(:\d+):\d+\)?$/;
- const lines = [];
- for ( let line of error.stack.split(/[\n\r]+/) ) {
- if ( line.includes(exceptionToken) ) { continue; }
- line = line.trim();
- const match = safe.RegExp_exec.call(reLine, line);
- if ( match === null ) { continue; }
- let url = match[2];
- if ( url.startsWith('(') ) { url = url.slice(1); }
- if ( url === docURL.href ) {
- url = 'inlineScript';
- } else if ( url.startsWith('') ) {
- url = 'injectedScript';
- }
- let fn = match[1] !== undefined
- ? match[1].slice(0, -1)
- : line.slice(0, match.index).trim();
- if ( fn.startsWith('at') ) { fn = fn.slice(2).trim(); }
- let rowcol = match[3];
- lines.push(' ' + `${fn} ${url}${rowcol}:1`.trim());
- }
- lines[0] = `stackDepth:${lines.length-1}`;
- const stack = lines.join('\t');
- const r = needleDetails.matchAll !== true &&
- safe.testPattern(needleDetails, stack);
- if (
- logLevel === 'all' ||
- logLevel === 'match' && r ||
- logLevel === 'nomatch' && !r
- ) {
- safe.uboLog(stack.replace(/\t/g, '\n'));
- }
- return r;
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'parse-properties-to-match.fn',
- fn: parsePropertiesToMatch,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function parsePropertiesToMatch(propsToMatch, implicit = '') {
- const safe = safeSelf();
- const needles = new Map();
- if ( propsToMatch === undefined || propsToMatch === '' ) { return needles; }
- const options = { canNegate: true };
- for ( const needle of propsToMatch.split(/\s+/) ) {
- const [ prop, pattern ] = needle.split(':');
- if ( prop === '' ) { continue; }
- if ( pattern !== undefined ) {
- needles.set(prop, safe.initPattern(pattern, options));
- } else if ( implicit !== '' ) {
- needles.set(implicit, safe.initPattern(prop, options));
- }
- }
- return needles;
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'match-object-properties.fn',
- fn: matchObjectProperties,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function matchObjectProperties(propNeedles, ...objs) {
- if ( matchObjectProperties.extractProperties === undefined ) {
- matchObjectProperties.extractProperties = (src, des, props) => {
- for ( const p of props ) {
- const v = src[p];
- if ( v === undefined ) { continue; }
- des[p] = src[p];
- }
- };
- }
- const safe = safeSelf();
- const haystack = {};
- const props = safe.Array_from(propNeedles.keys());
- for ( const obj of objs ) {
- if ( obj instanceof Object === false ) { continue; }
- matchObjectProperties.extractProperties(obj, haystack, props);
- }
- for ( const [ prop, details ] of propNeedles ) {
- let value = haystack[prop];
- if ( value === undefined ) { continue; }
- if ( typeof value !== 'string' ) {
- try { value = safe.JSON_stringify(value); }
- catch(ex) { }
- if ( typeof value !== 'string' ) { continue; }
- }
- if ( safe.testPattern(details, value) ) { continue; }
- return false;
- }
- return true;
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'json-prune-fetch-response.fn',
- fn: jsonPruneFetchResponseFn,
- dependencies: [
- 'match-object-properties.fn',
- 'object-prune.fn',
- 'parse-properties-to-match.fn',
- 'safe-self.fn',
- ],
-});
-function jsonPruneFetchResponseFn(
- rawPrunePaths = '',
- rawNeedlePaths = ''
-) {
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('json-prune-fetch-response', rawPrunePaths, rawNeedlePaths);
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 2);
- const propNeedles = parsePropertiesToMatch(extraArgs.propsToMatch, 'url');
- const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true });
- const logall = rawPrunePaths === '';
- const applyHandler = function(target, thisArg, args) {
- const fetchPromise = Reflect.apply(target, thisArg, args);
- let outcome = logall ? 'nomatch' : 'match';
- if ( propNeedles.size !== 0 ) {
- const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ];
- if ( objs[0] instanceof Request ) {
- try {
- objs[0] = safe.Request_clone.call(objs[0]);
- } catch(ex) {
- safe.uboErr(logPrefix, 'Error:', ex);
- }
- }
- if ( args[1] instanceof Object ) {
- objs.push(args[1]);
- }
- if ( matchObjectProperties(propNeedles, ...objs) === false ) {
- outcome = 'nomatch';
- }
- }
- if ( logall === false && outcome === 'nomatch' ) { return fetchPromise; }
- if ( safe.logLevel > 1 && outcome !== 'nomatch' && propNeedles.size !== 0 ) {
- safe.uboLog(logPrefix, `Matched optional "propsToMatch"\n${extraArgs.propsToMatch}`);
- }
- return fetchPromise.then(responseBefore => {
- const response = responseBefore.clone();
- return response.json().then(objBefore => {
- if ( typeof objBefore !== 'object' ) { return responseBefore; }
- if ( logall ) {
- safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2));
- return responseBefore;
- }
- const objAfter = objectPruneFn(
- objBefore,
- rawPrunePaths,
- rawNeedlePaths,
- stackNeedle,
- extraArgs
- );
- if ( typeof objAfter !== 'object' ) { return responseBefore; }
- safe.uboLog(logPrefix, 'Pruned');
- const responseAfter = Response.json(objAfter, {
- status: responseBefore.status,
- statusText: responseBefore.statusText,
- headers: responseBefore.headers,
- });
- Object.defineProperties(responseAfter, {
- ok: { value: responseBefore.ok },
- redirected: { value: responseBefore.redirected },
- type: { value: responseBefore.type },
- url: { value: responseBefore.url },
- });
- return responseAfter;
- }).catch(reason => {
- safe.uboErr(logPrefix, 'Error:', reason);
- return responseBefore;
- });
- }).catch(reason => {
- safe.uboErr(logPrefix, 'Error:', reason);
- return fetchPromise;
- });
- };
- self.fetch = new Proxy(self.fetch, {
- apply: applyHandler
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'replace-fetch-response.fn',
- fn: replaceFetchResponseFn,
- dependencies: [
- 'match-object-properties.fn',
- 'parse-properties-to-match.fn',
- 'safe-self.fn',
- ],
-});
-function replaceFetchResponseFn(
- trusted = false,
- pattern = '',
- replacement = '',
- propsToMatch = ''
-) {
- if ( trusted !== true ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('replace-fetch-response', pattern, replacement, propsToMatch);
- if ( pattern === '*' ) { pattern = '.*'; }
- const rePattern = safe.patternToRegex(pattern);
- const propNeedles = parsePropertiesToMatch(propsToMatch, 'url');
- self.fetch = new Proxy(self.fetch, {
- apply: function(target, thisArg, args) {
- const fetchPromise = Reflect.apply(target, thisArg, args);
- if ( pattern === '' ) { return fetchPromise; }
- let outcome = 'match';
- if ( propNeedles.size !== 0 ) {
- const objs = [ args[0] instanceof Object ? args[0] : { url: args[0] } ];
- if ( objs[0] instanceof Request ) {
- try {
- objs[0] = safe.Request_clone.call(objs[0]);
- }
- catch(ex) {
- safe.uboErr(logPrefix, ex);
- }
- }
- if ( args[1] instanceof Object ) {
- objs.push(args[1]);
- }
- if ( matchObjectProperties(propNeedles, ...objs) === false ) {
- outcome = 'nomatch';
- }
- }
- if ( outcome === 'nomatch' ) { return fetchPromise; }
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Matched "propsToMatch"\n${propsToMatch}`);
- }
- return fetchPromise.then(responseBefore => {
- const response = responseBefore.clone();
- return response.text().then(textBefore => {
- const textAfter = textBefore.replace(rePattern, replacement);
- const outcome = textAfter !== textBefore ? 'match' : 'nomatch';
- if ( outcome === 'nomatch' ) { return responseBefore; }
- safe.uboLog(logPrefix, 'Replaced');
- const responseAfter = new Response(textAfter, {
- status: responseBefore.status,
- statusText: responseBefore.statusText,
- headers: responseBefore.headers,
- });
- Object.defineProperties(responseAfter, {
- ok: { value: responseBefore.ok },
- redirected: { value: responseBefore.redirected },
- type: { value: responseBefore.type },
- url: { value: responseBefore.url },
- });
- return responseAfter;
- }).catch(reason => {
- safe.uboErr(logPrefix, reason);
- return responseBefore;
- });
- }).catch(reason => {
- safe.uboErr(logPrefix, reason);
- return fetchPromise;
- });
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'proxy-apply.fn',
- fn: proxyApplyFn,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function proxyApplyFn(
- target = '',
- handler = ''
-) {
- let context = globalThis;
- let prop = target;
- for (;;) {
- const pos = prop.indexOf('.');
- if ( pos === -1 ) { break; }
- context = context[prop.slice(0, pos)];
- if ( context instanceof Object === false ) { return; }
- prop = prop.slice(pos+1);
- }
- const fn = context[prop];
- if ( typeof fn !== 'function' ) { return; }
- if ( fn.prototype && fn.prototype.constructor === fn ) {
- context[prop] = new Proxy(fn, { construct: handler });
- return (...args) => { return Reflect.construct(...args); };
- }
- context[prop] = new Proxy(fn, { apply: handler });
- return (...args) => { return Reflect.apply(...args); };
-}
-
-/*******************************************************************************
-
- Injectable scriptlets
-
- These are meant to be used in the MAIN (webpage) execution world.
-
-*******************************************************************************/
-
-builtinScriptlets.push({
- name: 'abort-current-script.js',
- aliases: [
- 'acs.js',
- 'abort-current-inline-script.js',
- 'acis.js',
- ],
- fn: abortCurrentScript,
- dependencies: [
- 'abort-current-script-core.fn',
- 'run-at-html-element.fn',
- ],
-});
-// Issues to mind before changing anything:
-// https://github.com/uBlockOrigin/uBlock-issues/issues/2154
-function abortCurrentScript(...args) {
- runAtHtmlElementFn(( ) => {
- abortCurrentScriptCore(...args);
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'abort-on-property-read.js',
- aliases: [
- 'aopr.js',
- ],
- fn: abortOnPropertyRead,
- dependencies: [
- 'get-exception-token.fn',
- 'safe-self.fn',
- ],
-});
-function abortOnPropertyRead(
- chain = ''
-) {
- if ( typeof chain !== 'string' ) { return; }
- if ( chain === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('abort-on-property-read', chain);
- const exceptionToken = getExceptionToken();
- const abort = function() {
- safe.uboLog(logPrefix, 'Aborted');
- throw new ReferenceError(exceptionToken);
- };
- const makeProxy = function(owner, chain) {
- const pos = chain.indexOf('.');
- if ( pos === -1 ) {
- const desc = Object.getOwnPropertyDescriptor(owner, chain);
- if ( !desc || desc.get !== abort ) {
- Object.defineProperty(owner, chain, {
- get: abort,
- set: function(){}
- });
- }
- return;
- }
- const prop = chain.slice(0, pos);
- let v = owner[prop];
- chain = chain.slice(pos + 1);
- if ( v ) {
- makeProxy(v, chain);
- return;
- }
- const desc = Object.getOwnPropertyDescriptor(owner, prop);
- if ( desc && desc.set !== undefined ) { return; }
- Object.defineProperty(owner, prop, {
- get: function() { return v; },
- set: function(a) {
- v = a;
- if ( a instanceof Object ) {
- makeProxy(a, chain);
- }
- }
- });
- };
- const owner = window;
- makeProxy(owner, chain);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'abort-on-property-write.js',
- aliases: [
- 'aopw.js',
- ],
- fn: abortOnPropertyWrite,
- dependencies: [
- 'get-exception-token.fn',
- 'safe-self.fn',
- ],
-});
-function abortOnPropertyWrite(
- prop = ''
-) {
- if ( typeof prop !== 'string' ) { return; }
- if ( prop === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('abort-on-property-write', prop);
- const exceptionToken = getExceptionToken();
- let owner = window;
- for (;;) {
- const pos = prop.indexOf('.');
- if ( pos === -1 ) { break; }
- owner = owner[prop.slice(0, pos)];
- if ( owner instanceof Object === false ) { return; }
- prop = prop.slice(pos + 1);
- }
- delete owner[prop];
- Object.defineProperty(owner, prop, {
- set: function() {
- safe.uboLog(logPrefix, 'Aborted');
- throw new ReferenceError(exceptionToken);
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'abort-on-stack-trace.js',
- aliases: [
- 'aost.js',
- ],
- fn: abortOnStackTrace,
- dependencies: [
- 'get-exception-token.fn',
- 'matches-stack-trace.fn',
- 'safe-self.fn',
- ],
-});
-function abortOnStackTrace(
- chain = '',
- needle = ''
-) {
- if ( typeof chain !== 'string' ) { return; }
- const safe = safeSelf();
- const needleDetails = safe.initPattern(needle, { canNegate: true });
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 2);
- if ( needle === '' ) { extraArgs.log = 'all'; }
- const makeProxy = function(owner, chain) {
- const pos = chain.indexOf('.');
- if ( pos === -1 ) {
- let v = owner[chain];
- Object.defineProperty(owner, chain, {
- get: function() {
- if ( matchesStackTrace(needleDetails, extraArgs.log) ) {
- throw new ReferenceError(getExceptionToken());
- }
- return v;
- },
- set: function(a) {
- if ( matchesStackTrace(needleDetails, extraArgs.log) ) {
- throw new ReferenceError(getExceptionToken());
- }
- v = a;
- },
- });
- return;
- }
- const prop = chain.slice(0, pos);
- let v = owner[prop];
- chain = chain.slice(pos + 1);
- if ( v ) {
- makeProxy(v, chain);
- return;
- }
- const desc = Object.getOwnPropertyDescriptor(owner, prop);
- if ( desc && desc.set !== undefined ) { return; }
- Object.defineProperty(owner, prop, {
- get: function() { return v; },
- set: function(a) {
- v = a;
- if ( a instanceof Object ) {
- makeProxy(a, chain);
- }
- }
- });
- };
- const owner = window;
- makeProxy(owner, chain);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'addEventListener-defuser.js',
- aliases: [
- 'aeld.js',
- 'prevent-addEventListener.js',
- ],
- fn: addEventListenerDefuser,
- dependencies: [
- 'run-at.fn',
- 'safe-self.fn',
- 'should-debug.fn',
- ],
-});
-// https://github.com/uBlockOrigin/uAssets/issues/9123#issuecomment-848255120
-function addEventListenerDefuser(
- type = '',
- pattern = ''
-) {
- const safe = safeSelf();
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 2);
- const logPrefix = safe.makeLogPrefix('prevent-addEventListener', type, pattern);
- const reType = safe.patternToRegex(type, undefined, true);
- const rePattern = safe.patternToRegex(pattern);
- const debug = shouldDebug(extraArgs);
- const targetSelector = extraArgs.elements || undefined;
- const elementMatches = elem => {
- if ( elem && elem.matches && elem.matches(targetSelector) ) { return true; }
- const elems = Array.from(document.querySelectorAll(targetSelector));
- return elems.includes(elem);
- };
- const elementDetails = elem => {
- if ( elem instanceof Window ) { return 'window'; }
- if ( elem instanceof Document ) { return 'document'; }
- if ( elem instanceof Element === false ) { return '?'; }
- const parts = [];
- if ( elem.id !== '' ) { parts.push(`#${CSS.escape(elem.id)}`); }
- for ( let i = 0; i < elem.classList.length; i++ ) {
- parts.push(`.${CSS.escape(elem.classList.item(i))}`);
- }
- for ( let i = 0; i < elem.attributes.length; i++ ) {
- const attr = elem.attributes.item(i);
- if ( attr.name === 'id' ) { continue; }
- if ( attr.name === 'class' ) { continue; }
- parts.push(`[${CSS.escape(attr.name)}="${attr.value}"]`);
- }
- return parts.join('');
- };
- const shouldPrevent = (thisArg, type, handler) => {
- const matchesType = safe.RegExp_test.call(reType, type);
- const matchesHandler = safe.RegExp_test.call(rePattern, handler);
- const matchesEither = matchesType || matchesHandler;
- const matchesBoth = matchesType && matchesHandler;
- if ( debug === 1 && matchesBoth || debug === 2 && matchesEither ) {
- debugger; // eslint-disable-line no-debugger
- }
- if ( matchesBoth && targetSelector !== undefined ) {
- if ( elementMatches(thisArg) === false ) { return false; }
- }
- return matchesBoth;
- };
- const trapEddEventListeners = ( ) => {
- const eventListenerHandler = {
- apply: function(target, thisArg, args) {
- let t, h;
- try {
- t = String(args[0]);
- if ( typeof args[1] === 'function' ) {
- h = String(safe.Function_toString(args[1]));
- } else if ( typeof args[1] === 'object' && args[1] !== null ) {
- if ( typeof args[1].handleEvent === 'function' ) {
- h = String(safe.Function_toString(args[1].handleEvent));
- }
- } else {
- h = String(args[1]);
- }
- } catch(ex) {
- }
- if ( type === '' && pattern === '' ) {
- safe.uboLog(logPrefix, `Called: ${t}\n${h}\n${elementDetails(thisArg)}`);
- } else if ( shouldPrevent(thisArg, t, h) ) {
- return safe.uboLog(logPrefix, `Prevented: ${t}\n${h}\n${elementDetails(thisArg)}`);
- }
- return Reflect.apply(target, thisArg, args);
- },
- get(target, prop, receiver) {
- if ( prop === 'toString' ) {
- return target.toString.bind(target);
- }
- return Reflect.get(target, prop, receiver);
- },
- };
- self.EventTarget.prototype.addEventListener = new Proxy(
- self.EventTarget.prototype.addEventListener,
- eventListenerHandler
- );
- };
- runAt(( ) => {
- trapEddEventListeners();
- }, extraArgs.runAt);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'json-prune.js',
- fn: jsonPrune,
- dependencies: [
- 'object-prune.fn',
- 'safe-self.fn',
- ],
-});
-function jsonPrune(
- rawPrunePaths = '',
- rawNeedlePaths = '',
- stackNeedle = ''
-) {
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('json-prune', rawPrunePaths, rawNeedlePaths, stackNeedle);
- const stackNeedleDetails = safe.initPattern(stackNeedle, { canNegate: true });
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
- JSON.parse = new Proxy(JSON.parse, {
- apply: function(target, thisArg, args) {
- const objBefore = Reflect.apply(target, thisArg, args);
- if ( rawPrunePaths === '' ) {
- safe.uboLog(logPrefix, safe.JSON_stringify(objBefore, null, 2));
- }
- const objAfter = objectPruneFn(
- objBefore,
- rawPrunePaths,
- rawNeedlePaths,
- stackNeedleDetails,
- extraArgs
- );
- if ( objAfter === undefined ) { return objBefore; }
- safe.uboLog(logPrefix, 'Pruned');
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `After pruning:\n${safe.JSON_stringify(objAfter, null, 2)}`);
- }
- return objAfter;
- },
- });
-}
-
-/*******************************************************************************
- *
- * json-prune-fetch-response.js
- *
- * Prune JSON response of fetch requests.
- *
- **/
-
-builtinScriptlets.push({
- name: 'json-prune-fetch-response.js',
- fn: jsonPruneFetchResponse,
- dependencies: [
- 'json-prune-fetch-response.fn',
- ],
-});
-function jsonPruneFetchResponse(...args) {
- jsonPruneFetchResponseFn(...args);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'json-prune-xhr-response.js',
- fn: jsonPruneXhrResponse,
- dependencies: [
- 'match-object-properties.fn',
- 'object-prune.fn',
- 'parse-properties-to-match.fn',
- 'safe-self.fn',
- ],
-});
-function jsonPruneXhrResponse(
- rawPrunePaths = '',
- rawNeedlePaths = ''
-) {
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('json-prune-xhr-response', rawPrunePaths, rawNeedlePaths);
- const xhrInstances = new WeakMap();
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 2);
- const propNeedles = parsePropertiesToMatch(extraArgs.propsToMatch, 'url');
- const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true });
- self.XMLHttpRequest = class extends self.XMLHttpRequest {
- open(method, url, ...args) {
- const xhrDetails = { method, url };
- let outcome = 'match';
- if ( propNeedles.size !== 0 ) {
- if ( matchObjectProperties(propNeedles, xhrDetails) === false ) {
- outcome = 'nomatch';
- }
- }
- if ( outcome === 'match' ) {
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Matched optional "propsToMatch", "${extraArgs.propsToMatch}"`);
- }
- xhrInstances.set(this, xhrDetails);
- }
- return super.open(method, url, ...args);
- }
- get response() {
- const innerResponse = super.response;
- const xhrDetails = xhrInstances.get(this);
- if ( xhrDetails === undefined ) {
- return innerResponse;
- }
- const responseLength = typeof innerResponse === 'string'
- ? innerResponse.length
- : undefined;
- if ( xhrDetails.lastResponseLength !== responseLength ) {
- xhrDetails.response = undefined;
- xhrDetails.lastResponseLength = responseLength;
- }
- if ( xhrDetails.response !== undefined ) {
- return xhrDetails.response;
- }
- let objBefore;
- if ( typeof innerResponse === 'object' ) {
- objBefore = innerResponse;
- } else if ( typeof innerResponse === 'string' ) {
- try {
- objBefore = safe.JSON_parse(innerResponse);
- } catch(ex) {
- }
- }
- if ( typeof objBefore !== 'object' ) {
- return (xhrDetails.response = innerResponse);
- }
- const objAfter = objectPruneFn(
- objBefore,
- rawPrunePaths,
- rawNeedlePaths,
- stackNeedle,
- extraArgs
- );
- let outerResponse;
- if ( typeof objAfter === 'object' ) {
- outerResponse = typeof innerResponse === 'string'
- ? safe.JSON_stringify(objAfter)
- : objAfter;
- safe.uboLog(logPrefix, 'Pruned');
- } else {
- outerResponse = innerResponse;
- }
- return (xhrDetails.response = outerResponse);
- }
- get responseText() {
- const response = this.response;
- return typeof response !== 'string'
- ? super.responseText
- : response;
- }
- };
-}
-
-/******************************************************************************/
-
-// There is still code out there which uses `eval` in lieu of `JSON.parse`.
-
-builtinScriptlets.push({
- name: 'evaldata-prune.js',
- fn: evaldataPrune,
- dependencies: [
- 'object-prune.fn',
- ],
-});
-function evaldataPrune(
- rawPrunePaths = '',
- rawNeedlePaths = ''
-) {
- self.eval = new Proxy(self.eval, {
- apply(target, thisArg, args) {
- const before = Reflect.apply(target, thisArg, args);
- if ( typeof before === 'object' ) {
- const after = objectPruneFn(before, rawPrunePaths, rawNeedlePaths);
- return after || before;
- }
- return before;
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'adjust-setInterval.js',
- aliases: [
- 'nano-setInterval-booster.js',
- 'nano-sib.js',
- ],
- fn: adjustSetInterval,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-// Imported from:
-// https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L126
-//
-// Speed up or down setInterval, 3 optional arguments.
-// The payload matcher, a string literal or a JavaScript RegExp, defaults
-// to match all.
-// delayMatcher
-// The delay matcher, an integer, defaults to 1000.
-// Use `*` to match any delay.
-// boostRatio - The delay multiplier when there is a match, 0.5 speeds up by
-// 2 times and 2 slows down by 2 times, defaults to 0.05 or speed up
-// 20 times. Speed up and down both cap at 50 times.
-function adjustSetInterval(
- needleArg = '',
- delayArg = '',
- boostArg = ''
-) {
- if ( typeof needleArg !== 'string' ) { return; }
- const safe = safeSelf();
- const reNeedle = safe.patternToRegex(needleArg);
- let delay = delayArg !== '*' ? parseInt(delayArg, 10) : -1;
- if ( isNaN(delay) || isFinite(delay) === false ) { delay = 1000; }
- let boost = parseFloat(boostArg);
- boost = isNaN(boost) === false && isFinite(boost)
- ? Math.min(Math.max(boost, 0.001), 50)
- : 0.05;
- self.setInterval = new Proxy(self.setInterval, {
- apply: function(target, thisArg, args) {
- const [ a, b ] = args;
- if (
- (delay === -1 || b === delay) &&
- reNeedle.test(a.toString())
- ) {
- args[1] = b * boost;
- }
- return target.apply(thisArg, args);
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'adjust-setTimeout.js',
- aliases: [
- 'nano-setTimeout-booster.js',
- 'nano-stb.js',
- ],
- fn: adjustSetTimeout,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-// Imported from:
-// https://github.com/NanoAdblocker/NanoFilters/blob/1f3be7211bb0809c5106996f52564bf10c4525f7/NanoFiltersSource/NanoResources.txt#L82
-//
-// Speed up or down setTimeout, 3 optional arguments.
-// funcMatcher
-// The payload matcher, a string literal or a JavaScript RegExp, defaults
-// to match all.
-// delayMatcher
-// The delay matcher, an integer, defaults to 1000.
-// Use `*` to match any delay.
-// boostRatio - The delay multiplier when there is a match, 0.5 speeds up by
-// 2 times and 2 slows down by 2 times, defaults to 0.05 or speed up
-// 20 times. Speed up and down both cap at 50 times.
-function adjustSetTimeout(
- needleArg = '',
- delayArg = '',
- boostArg = ''
-) {
- if ( typeof needleArg !== 'string' ) { return; }
- const safe = safeSelf();
- const reNeedle = safe.patternToRegex(needleArg);
- let delay = delayArg !== '*' ? parseInt(delayArg, 10) : -1;
- if ( isNaN(delay) || isFinite(delay) === false ) { delay = 1000; }
- let boost = parseFloat(boostArg);
- boost = isNaN(boost) === false && isFinite(boost)
- ? Math.min(Math.max(boost, 0.001), 50)
- : 0.05;
- self.setTimeout = new Proxy(self.setTimeout, {
- apply: function(target, thisArg, args) {
- const [ a, b ] = args;
- if (
- (delay === -1 || b === delay) &&
- reNeedle.test(a.toString())
- ) {
- args[1] = b * boost;
- }
- return target.apply(thisArg, args);
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'noeval-if.js',
- aliases: [
- 'prevent-eval-if.js',
- ],
- fn: noEvalIf,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function noEvalIf(
- needle = ''
-) {
- if ( typeof needle !== 'string' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('noeval-if', needle);
- const reNeedle = safe.patternToRegex(needle);
- window.eval = new Proxy(window.eval, { // jshint ignore: line
- apply: function(target, thisArg, args) {
- const a = String(args[0]);
- if ( needle !== '' && reNeedle.test(a) ) {
- safe.uboLog(logPrefix, 'Prevented:\n', a);
- return;
- }
- if ( needle === '' || safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, 'Not prevented:\n', a);
- }
- return Reflect.apply(target, thisArg, args);
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'prevent-fetch.js',
- aliases: [
- 'no-fetch-if.js',
- ],
- fn: noFetchIf,
- dependencies: [
- 'generate-content.fn',
- 'safe-self.fn',
- ],
-});
-function noFetchIf(
- propsToMatch = '',
- responseBody = ''
-) {
- if ( typeof propsToMatch !== 'string' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('prevent-fetch', propsToMatch, responseBody);
- const needles = [];
- for ( const condition of propsToMatch.split(/\s+/) ) {
- if ( condition === '' ) { continue; }
- const pos = condition.indexOf(':');
- let key, value;
- if ( pos !== -1 ) {
- key = condition.slice(0, pos);
- value = condition.slice(pos + 1);
- } else {
- key = 'url';
- value = condition;
- }
- needles.push({ key, re: safe.patternToRegex(value) });
- }
- self.fetch = new Proxy(self.fetch, {
- apply: function(target, thisArg, args) {
- const details = args[0] instanceof self.Request
- ? args[0]
- : Object.assign({ url: args[0] }, args[1]);
- let proceed = true;
- try {
- const props = new Map();
- for ( const prop in details ) {
- let v = details[prop];
- if ( typeof v !== 'string' ) {
- try { v = safe.JSON_stringify(v); }
- catch(ex) { }
- }
- if ( typeof v !== 'string' ) { continue; }
- props.set(prop, v);
- }
- if ( propsToMatch === '' && responseBody === '' ) {
- const out = Array.from(props).map(a => `${a[0]}:${a[1]}`);
- safe.uboLog(logPrefix, `Called: ${out.join('\n')}`);
- return Reflect.apply(target, thisArg, args);
- }
- proceed = needles.length === 0;
- for ( const { key, re } of needles ) {
- if (
- props.has(key) === false ||
- re.test(props.get(key)) === false
- ) {
- proceed = true;
- break;
- }
- }
- } catch(ex) {
- }
- if ( proceed ) {
- return Reflect.apply(target, thisArg, args);
- }
- let responseType = '';
- if ( details.mode === undefined || details.mode === 'cors' ) {
- try {
- const desURL = new URL(details.url);
- responseType = desURL.origin !== document.location.origin
- ? 'cors'
- : 'basic';
- } catch(ex) {
- safe.uboErr(logPrefix, `Error: ${ex}`);
- }
- }
- return generateContentFn(responseBody).then(text => {
- safe.uboLog(logPrefix, `Prevented with response "${text}"`);
- const response = new Response(text, {
- statusText: 'OK',
- headers: {
- 'Content-Length': text.length,
- }
- });
- safe.Object_defineProperty(response, 'url', {
- value: details.url
- });
- if ( responseType !== '' ) {
- safe.Object_defineProperty(response, 'type', {
- value: responseType
- });
- }
- return response;
- });
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'prevent-refresh.js',
- aliases: [
- 'refresh-defuser.js',
- ],
- fn: preventRefresh,
- world: 'ISOLATED',
- dependencies: [
- 'run-at.fn',
- 'safe-self.fn',
- ],
-});
-// https://www.reddit.com/r/uBlockOrigin/comments/q0frv0/while_reading_a_sports_article_i_was_redirected/hf7wo9v/
-function preventRefresh(
- arg1 = ''
-) {
- if ( typeof arg1 !== 'string' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('prevent-refresh', arg1);
- const defuse = ( ) => {
- const meta = document.querySelector('meta[http-equiv="refresh" i][content]');
- if ( meta === null ) { return; }
- safe.uboLog(logPrefix, `Prevented "${meta.textContent}"`);
- const s = arg1 === ''
- ? meta.getAttribute('content')
- : arg1;
- const ms = Math.max(parseFloat(s) || 0, 0) * 1000;
- setTimeout(( ) => { window.stop(); }, ms);
- };
- runAt(( ) => {
- defuse();
- }, 'interactive');
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'remove-attr.js',
- aliases: [
- 'ra.js',
- ],
- fn: removeAttr,
- dependencies: [
- 'run-at.fn',
- 'safe-self.fn',
- ],
-});
-function removeAttr(
- rawToken = '',
- rawSelector = '',
- behavior = ''
-) {
- if ( typeof rawToken !== 'string' ) { return; }
- if ( rawToken === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('remove-attr', rawToken, rawSelector, behavior);
- const tokens = rawToken.split(/\s*\|\s*/);
- const selector = tokens
- .map(a => `${rawSelector}[${CSS.escape(a)}]`)
- .join(',');
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Target selector:\n\t${selector}`);
- }
- let timer;
- const rmattr = ( ) => {
- timer = undefined;
- try {
- const nodes = document.querySelectorAll(selector);
- for ( const node of nodes ) {
- for ( const attr of tokens ) {
- if ( node.hasAttribute(attr) === false ) { continue; }
- node.removeAttribute(attr);
- safe.uboLog(logPrefix, `Removed attribute '${attr}'`);
- }
- }
- } catch(ex) {
- }
- };
- const mutationHandler = mutations => {
- if ( timer !== undefined ) { return; }
- let skip = true;
- for ( let i = 0; i < mutations.length && skip; i++ ) {
- const { type, addedNodes, removedNodes } = mutations[i];
- if ( type === 'attributes' ) { skip = false; }
- for ( let j = 0; j < addedNodes.length && skip; j++ ) {
- if ( addedNodes[j].nodeType === 1 ) { skip = false; break; }
- }
- for ( let j = 0; j < removedNodes.length && skip; j++ ) {
- if ( removedNodes[j].nodeType === 1 ) { skip = false; break; }
- }
- }
- if ( skip ) { return; }
- timer = safe.onIdle(rmattr, { timeout: 67 });
- };
- const start = ( ) => {
- rmattr();
- if ( /\bstay\b/.test(behavior) === false ) { return; }
- const observer = new MutationObserver(mutationHandler);
- observer.observe(document, {
- attributes: true,
- attributeFilter: tokens,
- childList: true,
- subtree: true,
- });
- };
- runAt(( ) => {
- start();
- }, /\bcomplete\b/.test(behavior) ? 'idle' : 'interactive');
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'remove-class.js',
- aliases: [
- 'rc.js',
- ],
- fn: removeClass,
- world: 'ISOLATED',
- dependencies: [
- 'run-at.fn',
- 'safe-self.fn',
- ],
-});
-function removeClass(
- rawToken = '',
- rawSelector = '',
- behavior = ''
-) {
- if ( typeof rawToken !== 'string' ) { return; }
- if ( rawToken === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('remove-class', rawToken, rawSelector, behavior);
- const tokens = rawToken.split(/\s*\|\s*/);
- const selector = tokens
- .map(a => `${rawSelector}.${CSS.escape(a)}`)
- .join(',');
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Target selector:\n\t${selector}`);
- }
- const mustStay = /\bstay\b/.test(behavior);
- let timer;
- const rmclass = ( ) => {
- timer = undefined;
- try {
- const nodes = document.querySelectorAll(selector);
- for ( const node of nodes ) {
- node.classList.remove(...tokens);
- safe.uboLog(logPrefix, 'Removed class(es)');
- }
- } catch(ex) {
- }
- if ( mustStay ) { return; }
- if ( document.readyState !== 'complete' ) { return; }
- observer.disconnect();
- };
- const mutationHandler = mutations => {
- if ( timer !== undefined ) { return; }
- let skip = true;
- for ( let i = 0; i < mutations.length && skip; i++ ) {
- const { type, addedNodes, removedNodes } = mutations[i];
- if ( type === 'attributes' ) { skip = false; }
- for ( let j = 0; j < addedNodes.length && skip; j++ ) {
- if ( addedNodes[j].nodeType === 1 ) { skip = false; break; }
- }
- for ( let j = 0; j < removedNodes.length && skip; j++ ) {
- if ( removedNodes[j].nodeType === 1 ) { skip = false; break; }
- }
- }
- if ( skip ) { return; }
- timer = safe.onIdle(rmclass, { timeout: 67 });
- };
- const observer = new MutationObserver(mutationHandler);
- const start = ( ) => {
- rmclass();
- observer.observe(document, {
- attributes: true,
- attributeFilter: [ 'class' ],
- childList: true,
- subtree: true,
- });
- };
- runAt(( ) => {
- start();
- }, /\bcomplete\b/.test(behavior) ? 'idle' : 'loading');
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'no-requestAnimationFrame-if.js',
- aliases: [
- 'norafif.js',
- 'prevent-requestAnimationFrame.js',
- ],
- fn: noRequestAnimationFrameIf,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function noRequestAnimationFrameIf(
- needle = ''
-) {
- if ( typeof needle !== 'string' ) { return; }
- const safe = safeSelf();
- const needleNot = needle.charAt(0) === '!';
- if ( needleNot ) { needle = needle.slice(1); }
- const log = needleNot === false && needle === '' ? console.log : undefined;
- const reNeedle = safe.patternToRegex(needle);
- window.requestAnimationFrame = new Proxy(window.requestAnimationFrame, {
- apply: function(target, thisArg, args) {
- const a = args[0] instanceof Function
- ? String(safe.Function_toString(args[0]))
- : String(args[0]);
- let defuse = false;
- if ( log !== undefined ) {
- log('uBO: requestAnimationFrame("%s")', a);
- } else {
- defuse = reNeedle.test(a) !== needleNot;
- }
- if ( defuse ) {
- args[0] = function(){};
- }
- return target.apply(thisArg, args);
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'set-constant.js',
- aliases: [
- 'set.js',
- ],
- fn: setConstant,
- dependencies: [
- 'set-constant.fn'
- ],
-});
-function setConstant(
- ...args
-) {
- setConstantFn(false, ...args);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'no-setInterval-if.js',
- aliases: [
- 'nosiif.js',
- 'prevent-setInterval.js',
- 'setInterval-defuser.js',
- ],
- fn: noSetIntervalIf,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function noSetIntervalIf(
- needle = '',
- delay = ''
-) {
- if ( typeof needle !== 'string' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('prevent-setInterval', needle, delay);
- const needleNot = needle.charAt(0) === '!';
- if ( needleNot ) { needle = needle.slice(1); }
- if ( delay === '' ) { delay = undefined; }
- let delayNot = false;
- if ( delay !== undefined ) {
- delayNot = delay.charAt(0) === '!';
- if ( delayNot ) { delay = delay.slice(1); }
- delay = parseInt(delay, 10);
- }
- const reNeedle = safe.patternToRegex(needle);
- self.setInterval = new Proxy(self.setInterval, {
- apply: function(target, thisArg, args) {
- const a = args[0] instanceof Function
- ? String(safe.Function_toString(args[0]))
- : String(args[0]);
- const b = args[1];
- if ( needle === '' && delay === undefined ) {
- safe.uboLog(logPrefix, `Called:\n${a}\n${b}`);
- return Reflect.apply(target, thisArg, args);
- }
- let defuse;
- if ( needle !== '' ) {
- defuse = reNeedle.test(a) !== needleNot;
- }
- if ( defuse !== false && delay !== undefined ) {
- defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot;
- }
- if ( defuse ) {
- args[0] = function(){};
- safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`);
- }
- return Reflect.apply(target, thisArg, args);
- },
- get(target, prop, receiver) {
- if ( prop === 'toString' ) {
- return target.toString.bind(target);
- }
- return Reflect.get(target, prop, receiver);
- },
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'no-setTimeout-if.js',
- aliases: [
- 'nostif.js',
- 'prevent-setTimeout.js',
- 'setTimeout-defuser.js',
- ],
- fn: noSetTimeoutIf,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function noSetTimeoutIf(
- needle = '',
- delay = ''
-) {
- if ( typeof needle !== 'string' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('prevent-setTimeout', needle, delay);
- const needleNot = needle.charAt(0) === '!';
- if ( needleNot ) { needle = needle.slice(1); }
- if ( delay === '' ) { delay = undefined; }
- let delayNot = false;
- if ( delay !== undefined ) {
- delayNot = delay.charAt(0) === '!';
- if ( delayNot ) { delay = delay.slice(1); }
- delay = parseInt(delay, 10);
- }
- const reNeedle = safe.patternToRegex(needle);
- self.setTimeout = new Proxy(self.setTimeout, {
- apply: function(target, thisArg, args) {
- const a = args[0] instanceof Function
- ? String(safe.Function_toString(args[0]))
- : String(args[0]);
- const b = args[1];
- if ( needle === '' && delay === undefined ) {
- safe.uboLog(logPrefix, `Called:\n${a}\n${b}`);
- return Reflect.apply(target, thisArg, args);
- }
- let defuse;
- if ( needle !== '' ) {
- defuse = reNeedle.test(a) !== needleNot;
- }
- if ( defuse !== false && delay !== undefined ) {
- defuse = (b === delay || isNaN(b) && isNaN(delay) ) !== delayNot;
- }
- if ( defuse ) {
- args[0] = function(){};
- safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`);
- }
- return Reflect.apply(target, thisArg, args);
- },
- get(target, prop, receiver) {
- if ( prop === 'toString' ) {
- return target.toString.bind(target);
- }
- return Reflect.get(target, prop, receiver);
- },
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'webrtc-if.js',
- fn: webrtcIf,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function webrtcIf(
- good = ''
-) {
- if ( typeof good !== 'string' ) { return; }
- const safe = safeSelf();
- const reGood = safe.patternToRegex(good);
- const rtcName = window.RTCPeerConnection
- ? 'RTCPeerConnection'
- : (window.webkitRTCPeerConnection ? 'webkitRTCPeerConnection' : '');
- if ( rtcName === '' ) { return; }
- const log = console.log.bind(console);
- const neuteredPeerConnections = new WeakSet();
- const isGoodConfig = function(instance, config) {
- if ( neuteredPeerConnections.has(instance) ) { return false; }
- if ( config instanceof Object === false ) { return true; }
- if ( Array.isArray(config.iceServers) === false ) { return true; }
- for ( const server of config.iceServers ) {
- const urls = typeof server.urls === 'string'
- ? [ server.urls ]
- : server.urls;
- if ( Array.isArray(urls) ) {
- for ( const url of urls ) {
- if ( reGood.test(url) ) { return true; }
- }
- }
- if ( typeof server.username === 'string' ) {
- if ( reGood.test(server.username) ) { return true; }
- }
- if ( typeof server.credential === 'string' ) {
- if ( reGood.test(server.credential) ) { return true; }
- }
- }
- neuteredPeerConnections.add(instance);
- return false;
- };
- const peerConnectionCtor = window[rtcName];
- const peerConnectionProto = peerConnectionCtor.prototype;
- peerConnectionProto.createDataChannel =
- new Proxy(peerConnectionProto.createDataChannel, {
- apply: function(target, thisArg, args) {
- if ( isGoodConfig(target, args[1]) === false ) {
- log('uBO:', args[1]);
- return Reflect.apply(target, thisArg, args.slice(0, 1));
- }
- return Reflect.apply(target, thisArg, args);
- },
- });
- window[rtcName] =
- new Proxy(peerConnectionCtor, {
- construct: function(target, args) {
- if ( isGoodConfig(target, args[0]) === false ) {
- log('uBO:', args[0]);
- return Reflect.construct(target);
- }
- return Reflect.construct(target, args);
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'no-xhr-if.js',
- aliases: [
- 'prevent-xhr.js',
- ],
- fn: noXhrIf,
- dependencies: [
- 'generate-content.fn',
- 'match-object-properties.fn',
- 'parse-properties-to-match.fn',
- 'safe-self.fn',
- ],
-});
-function noXhrIf(
- propsToMatch = '',
- directive = ''
-) {
- if ( typeof propsToMatch !== 'string' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('prevent-xhr', propsToMatch, directive);
- const xhrInstances = new WeakMap();
- const propNeedles = parsePropertiesToMatch(propsToMatch, 'url');
- const warOrigin = scriptletGlobals.warOrigin;
- const headers = {
- 'date': '',
- 'content-type': '',
- 'content-length': '',
- };
- self.XMLHttpRequest = class extends self.XMLHttpRequest {
- open(method, url, ...args) {
- xhrInstances.delete(this);
- if ( warOrigin !== undefined && url.startsWith(warOrigin) ) {
- return super.open(method, url, ...args);
- }
- const haystack = { method, url };
- if ( propsToMatch === '' && directive === '' ) {
- safe.uboLog(logPrefix, `Called: ${safe.JSON_stringify(haystack, null, 2)}`);
- return super.open(method, url, ...args);
- }
- if ( matchObjectProperties(propNeedles, haystack) ) {
- xhrInstances.set(this, haystack);
- }
- haystack.headers = Object.assign({}, headers);
- return super.open(method, url, ...args);
- }
- send(...args) {
- const haystack = xhrInstances.get(this);
- if ( haystack === undefined ) {
- return super.send(...args);
- }
- haystack.headers['date'] = (new Date()).toUTCString();
- let promise = Promise.resolve({
- xhr: this,
- directive,
- props: {
- readyState: { value: 4 },
- response: { value: '' },
- responseText: { value: '' },
- responseXML: { value: null },
- responseURL: { value: haystack.url },
- status: { value: 200 },
- statusText: { value: 'OK' },
- },
- });
- switch ( this.responseType ) {
- case 'arraybuffer':
- promise = promise.then(details => {
- details.props.response.value = new ArrayBuffer(0);
- return details;
- });
- haystack.headers['content-type'] = 'application/octet-stream';
- break;
- case 'blob':
- promise = promise.then(details => {
- details.props.response.value = new Blob([]);
- return details;
- });
- haystack.headers['content-type'] = 'application/octet-stream';
- break;
- case 'document': {
- promise = promise.then(details => {
- const parser = new DOMParser();
- const doc = parser.parseFromString('', 'text/html');
- details.props.response.value = doc;
- details.props.responseXML.value = doc;
- return details;
- });
- haystack.headers['content-type'] = 'text/html';
- break;
- }
- case 'json':
- promise = promise.then(details => {
- details.props.response.value = {};
- details.props.responseText.value = '{}';
- return details;
- });
- haystack.headers['content-type'] = 'application/json';
- break;
- default:
- if ( directive === '' ) { break; }
- promise = promise.then(details => {
- return generateContentFn(details.directive).then(text => {
- details.props.response.value = text;
- details.props.responseText.value = text;
- return details;
- });
- });
- haystack.headers['content-type'] = 'text/plain';
- break;
- }
- promise.then(details => {
- haystack.headers['content-length'] = `${details.props.response.value}`.length;
- Object.defineProperties(details.xhr, details.props);
- details.xhr.dispatchEvent(new Event('readystatechange'));
- details.xhr.dispatchEvent(new Event('load'));
- details.xhr.dispatchEvent(new Event('loadend'));
- safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`);
- });
- }
- getResponseHeader(headerName) {
- const haystack = xhrInstances.get(this);
- if ( haystack === undefined || this.readyState < this.HEADERS_RECEIVED ) {
- return super.getResponseHeader(headerName);
- }
- const value = haystack.headers[headerName.toLowerCase()];
- if ( value !== undefined && value !== '' ) { return value; }
- return null;
- }
- getAllResponseHeaders() {
- const haystack = xhrInstances.get(this);
- if ( haystack === undefined || this.readyState < this.HEADERS_RECEIVED ) {
- return super.getAllResponseHeaders();
- }
- const out = [];
- for ( const [ name, value ] of Object.entries(haystack.headers) ) {
- if ( !value ) { continue; }
- out.push(`${name}: ${value}`);
- }
- if ( out.length !== 0 ) { out.push(''); }
- return out.join('\r\n');
- }
- };
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'no-window-open-if.js',
- aliases: [
- 'nowoif.js',
- 'prevent-window-open.js',
- 'window.open-defuser.js',
- ],
- fn: noWindowOpenIf,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function noWindowOpenIf(
- pattern = '',
- delay = '',
- decoy = ''
-) {
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('no-window-open-if', pattern, delay, decoy);
- const targetMatchResult = pattern.startsWith('!') === false;
- if ( targetMatchResult === false ) {
- pattern = pattern.slice(1);
- }
- const rePattern = safe.patternToRegex(pattern);
- let autoRemoveAfter = parseInt(delay);
- if ( isNaN(autoRemoveAfter) ) {
- autoRemoveAfter = -1;
- }
- const createDecoy = function(tag, urlProp, url) {
- const decoyElem = document.createElement(tag);
- decoyElem[urlProp] = url;
- decoyElem.style.setProperty('height','1px', 'important');
- decoyElem.style.setProperty('position','fixed', 'important');
- decoyElem.style.setProperty('top','-1px', 'important');
- decoyElem.style.setProperty('width','1px', 'important');
- document.body.appendChild(decoyElem);
- setTimeout(( ) => { decoyElem.remove(); }, autoRemoveAfter * 1000);
- return decoyElem;
- };
- window.open = new Proxy(window.open, {
- apply: function(target, thisArg, args) {
- const haystack = args.join(' ');
- if ( rePattern.test(haystack) !== targetMatchResult ) {
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Allowed (${args.join(', ')})`);
- }
- return Reflect.apply(target, thisArg, args);
- }
- safe.uboLog(logPrefix, `Prevented (${args.join(', ')})`);
- if ( autoRemoveAfter < 0 ) { return null; }
- const decoyElem = decoy === 'obj'
- ? createDecoy('object', 'data', ...args)
- : createDecoy('iframe', 'src', ...args);
- let popup = decoyElem.contentWindow;
- if ( typeof popup === 'object' && popup !== null ) {
- Object.defineProperty(popup, 'closed', { value: false });
- } else {
- const noopFunc = (function(){}).bind(self);
- popup = new Proxy(self, {
- get: function(target, prop) {
- if ( prop === 'closed' ) { return false; }
- const r = Reflect.get(...arguments);
- if ( typeof r === 'function' ) { return noopFunc; }
- return target[prop];
- },
- set: function() {
- return Reflect.set(...arguments);
- },
- });
- }
- if ( safe.logLevel !== 0 ) {
- popup = new Proxy(popup, {
- get: function(target, prop) {
- safe.uboLog(logPrefix, 'window.open / get', prop, '===', target[prop]);
- return Reflect.get(...arguments);
- },
- set: function(target, prop, value) {
- safe.uboLog(logPrefix, 'window.open / set', prop, '=', value);
- return Reflect.set(...arguments);
- },
- });
- }
- return popup;
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'close-window.js',
- aliases: [
- 'window-close-if.js',
- ],
- fn: closeWindow,
- world: 'ISOLATED',
- dependencies: [
- 'safe-self.fn',
- ],
-});
-// https://github.com/uBlockOrigin/uAssets/issues/10323#issuecomment-992312847
-// https://github.com/AdguardTeam/Scriptlets/issues/158
-// https://github.com/uBlockOrigin/uBlock-issues/discussions/2270
-function closeWindow(
- arg1 = ''
-) {
- if ( typeof arg1 !== 'string' ) { return; }
- const safe = safeSelf();
- let subject = '';
- if ( /^\/.*\/$/.test(arg1) ) {
- subject = window.location.href;
- } else if ( arg1 !== '' ) {
- subject = `${window.location.pathname}${window.location.search}`;
- }
- try {
- const re = safe.patternToRegex(arg1);
- if ( re.test(subject) ) {
- window.close();
- }
- } catch(ex) {
- console.log(ex);
- }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'window.name-defuser.js',
- fn: windowNameDefuser,
-});
-// https://github.com/gorhill/uBlock/issues/1228
-function windowNameDefuser() {
- if ( window === window.top ) {
- window.name = '';
- }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'overlay-buster.js',
- fn: overlayBuster,
-});
-// Experimental: Generic nuisance overlay buster.
-// if this works well and proves to be useful, this may end up
-// as a stock tool in uBO's popup panel.
-function overlayBuster() {
- if ( window !== window.top ) { return; }
- var tstart;
- var ttl = 30000;
- var delay = 0;
- var delayStep = 50;
- var buster = function() {
- var docEl = document.documentElement,
- bodyEl = document.body,
- vw = Math.min(docEl.clientWidth, window.innerWidth),
- vh = Math.min(docEl.clientHeight, window.innerHeight),
- tol = Math.min(vw, vh) * 0.05,
- el = document.elementFromPoint(vw/2, vh/2),
- style, rect;
- for (;;) {
- if ( el === null || el.parentNode === null || el === bodyEl ) {
- break;
- }
- style = window.getComputedStyle(el);
- if ( parseInt(style.zIndex, 10) >= 1000 || style.position === 'fixed' ) {
- rect = el.getBoundingClientRect();
- if ( rect.left <= tol && rect.top <= tol && (vw - rect.right) <= tol && (vh - rect.bottom) < tol ) {
- el.parentNode.removeChild(el);
- tstart = Date.now();
- el = document.elementFromPoint(vw/2, vh/2);
- bodyEl.style.setProperty('overflow', 'auto', 'important');
- docEl.style.setProperty('overflow', 'auto', 'important');
- continue;
- }
- }
- el = el.parentNode;
- }
- if ( (Date.now() - tstart) < ttl ) {
- delay = Math.min(delay + delayStep, 1000);
- setTimeout(buster, delay);
- }
- };
- var domReady = function(ev) {
- if ( ev ) {
- document.removeEventListener(ev.type, domReady);
- }
- tstart = Date.now();
- setTimeout(buster, delay);
- };
- if ( document.readyState === 'loading' ) {
- document.addEventListener('DOMContentLoaded', domReady);
- } else {
- domReady();
- }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'alert-buster.js',
- fn: alertBuster,
-});
-// https://github.com/uBlockOrigin/uAssets/issues/8
-function alertBuster() {
- window.alert = new Proxy(window.alert, {
- apply: function(a) {
- console.info(a);
- },
- get(target, prop, receiver) {
- if ( prop === 'toString' ) {
- return target.toString.bind(target);
- }
- return Reflect.get(target, prop, receiver);
- },
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'nowebrtc.js',
- fn: noWebrtc,
-});
-// Prevent web pages from using RTCPeerConnection(), and report attempts in console.
-function noWebrtc() {
- var rtcName = window.RTCPeerConnection ? 'RTCPeerConnection' : (
- window.webkitRTCPeerConnection ? 'webkitRTCPeerConnection' : ''
- );
- if ( rtcName === '' ) { return; }
- var log = console.log.bind(console);
- var pc = function(cfg) {
- log('Document tried to create an RTCPeerConnection: %o', cfg);
- };
- const noop = function() {
- };
- pc.prototype = {
- close: noop,
- createDataChannel: noop,
- createOffer: noop,
- setRemoteDescription: noop,
- toString: function() {
- return '[object RTCPeerConnection]';
- }
- };
- var z = window[rtcName];
- window[rtcName] = pc.bind(window);
- if ( z.prototype ) {
- z.prototype.createDataChannel = function() {
- return {
- close: function() {},
- send: function() {}
- };
- }.bind(null);
- }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'disable-newtab-links.js',
- fn: disableNewtabLinks,
-});
-// https://github.com/uBlockOrigin/uAssets/issues/913
-function disableNewtabLinks() {
- document.addEventListener('click', function(ev) {
- var target = ev.target;
- while ( target !== null ) {
- if ( target.localName === 'a' && target.hasAttribute('target') ) {
- ev.stopPropagation();
- ev.preventDefault();
- break;
- }
- target = target.parentNode;
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'remove-cookie.js',
- aliases: [
- 'cookie-remover.js',
- ],
- fn: cookieRemover,
- world: 'ISOLATED',
- dependencies: [
- 'safe-self.fn',
- ],
-});
-// https://github.com/NanoAdblocker/NanoFilters/issues/149
-function cookieRemover(
- needle = ''
-) {
- if ( typeof needle !== 'string' ) { return; }
- const safe = safeSelf();
- const reName = safe.patternToRegex(needle);
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 1);
- const throttle = (fn, ms = 500) => {
- if ( throttle.timer !== undefined ) { return; }
- throttle.timer = setTimeout(( ) => {
- throttle.timer = undefined;
- fn();
- }, ms);
- };
- const removeCookie = ( ) => {
- document.cookie.split(';').forEach(cookieStr => {
- const pos = cookieStr.indexOf('=');
- if ( pos === -1 ) { return; }
- const cookieName = cookieStr.slice(0, pos).trim();
- if ( reName.test(cookieName) === false ) { return; }
- const part1 = cookieName + '=';
- const part2a = '; domain=' + document.location.hostname;
- const part2b = '; domain=.' + document.location.hostname;
- let part2c, part2d;
- const domain = document.domain;
- if ( domain ) {
- if ( domain !== document.location.hostname ) {
- part2c = '; domain=.' + domain;
- }
- if ( domain.startsWith('www.') ) {
- part2d = '; domain=' + domain.replace('www', '');
- }
- }
- const part3 = '; path=/';
- const part4 = '; Max-Age=-1000; expires=Thu, 01 Jan 1970 00:00:00 GMT';
- document.cookie = part1 + part4;
- document.cookie = part1 + part2a + part4;
- document.cookie = part1 + part2b + part4;
- document.cookie = part1 + part3 + part4;
- document.cookie = part1 + part2a + part3 + part4;
- document.cookie = part1 + part2b + part3 + part4;
- if ( part2c !== undefined ) {
- document.cookie = part1 + part2c + part3 + part4;
- }
- if ( part2d !== undefined ) {
- document.cookie = part1 + part2d + part3 + part4;
- }
- });
- };
- removeCookie();
- window.addEventListener('beforeunload', removeCookie);
- if ( typeof extraArgs.when !== 'string' ) { return; }
- const supportedEventTypes = [ 'scroll', 'keydown' ];
- const eventTypes = extraArgs.when.split(/\s/);
- for ( const type of eventTypes ) {
- if ( supportedEventTypes.includes(type) === false ) { continue; }
- document.addEventListener(type, ( ) => {
- throttle(removeCookie);
- }, { passive: true });
- }
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'xml-prune.js',
- fn: xmlPrune,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function xmlPrune(
- selector = '',
- selectorCheck = '',
- urlPattern = ''
-) {
- if ( typeof selector !== 'string' ) { return; }
- if ( selector === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('xml-prune', selector, selectorCheck, urlPattern);
- const reUrl = safe.patternToRegex(urlPattern);
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
- const queryAll = (xmlDoc, selector) => {
- const isXpath = /^xpath\(.+\)$/.test(selector);
- if ( isXpath === false ) {
- return Array.from(xmlDoc.querySelectorAll(selector));
- }
- const xpr = xmlDoc.evaluate(
- selector.slice(6, -1),
- xmlDoc,
- null,
- XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
- null
- );
- const out = [];
- for ( let i = 0; i < xpr.snapshotLength; i++ ) {
- const node = xpr.snapshotItem(i);
- out.push(node);
- }
- return out;
- };
- const pruneFromDoc = xmlDoc => {
- try {
- if ( selectorCheck !== '' && xmlDoc.querySelector(selectorCheck) === null ) {
- return xmlDoc;
- }
- if ( extraArgs.logdoc ) {
- const serializer = new XMLSerializer();
- safe.uboLog(logPrefix, `Document is\n\t${serializer.serializeToString(xmlDoc)}`);
- }
- const items = queryAll(xmlDoc, selector);
- if ( items.length === 0 ) { return xmlDoc; }
- safe.uboLog(logPrefix, `Removing ${items.length} items`);
- for ( const item of items ) {
- if ( item.nodeType === 1 ) {
- item.remove();
- } else if ( item.nodeType === 2 ) {
- item.ownerElement.removeAttribute(item.nodeName);
- }
- safe.uboLog(logPrefix, `${item.constructor.name}.${item.nodeName} removed`);
- }
- } catch(ex) {
- safe.uboErr(logPrefix, `Error: ${ex}`);
- }
- return xmlDoc;
- };
- const pruneFromText = text => {
- if ( (/^\s*\s*$/.test(text)) === false ) {
- return text;
- }
- try {
- const xmlParser = new DOMParser();
- const xmlDoc = xmlParser.parseFromString(text, 'text/xml');
- pruneFromDoc(xmlDoc);
- const serializer = new XMLSerializer();
- text = serializer.serializeToString(xmlDoc);
- } catch(ex) {
- }
- return text;
- };
- const urlFromArg = arg => {
- if ( typeof arg === 'string' ) { return arg; }
- if ( arg instanceof Request ) { return arg.url; }
- return String(arg);
- };
- self.fetch = new Proxy(self.fetch, {
- apply: function(target, thisArg, args) {
- const fetchPromise = Reflect.apply(target, thisArg, args);
- if ( reUrl.test(urlFromArg(args[0])) === false ) {
- return fetchPromise;
- }
- return fetchPromise.then(responseBefore => {
- const response = responseBefore.clone();
- return response.text().then(text => {
- const responseAfter = new Response(pruneFromText(text), {
- status: responseBefore.status,
- statusText: responseBefore.statusText,
- headers: responseBefore.headers,
- });
- Object.defineProperties(responseAfter, {
- ok: { value: responseBefore.ok },
- redirected: { value: responseBefore.redirected },
- type: { value: responseBefore.type },
- url: { value: responseBefore.url },
- });
- return responseAfter;
- }).catch(( ) =>
- responseBefore
- );
- });
- }
- });
- self.XMLHttpRequest.prototype.open = new Proxy(self.XMLHttpRequest.prototype.open, {
- apply: async (target, thisArg, args) => {
- if ( reUrl.test(urlFromArg(args[1])) === false ) {
- return Reflect.apply(target, thisArg, args);
- }
- thisArg.addEventListener('readystatechange', function() {
- if ( thisArg.readyState !== 4 ) { return; }
- const type = thisArg.responseType;
- if (
- type === 'document' ||
- type === '' && thisArg.responseXML instanceof XMLDocument
- ) {
- pruneFromDoc(thisArg.responseXML);
- const serializer = new XMLSerializer();
- const textout = serializer.serializeToString(thisArg.responseXML);
- Object.defineProperty(thisArg, 'responseText', { value: textout });
- return;
- }
- if (
- type === 'text' ||
- type === '' && typeof thisArg.responseText === 'string'
- ) {
- const textin = thisArg.responseText;
- const textout = pruneFromText(textin);
- if ( textout === textin ) { return; }
- Object.defineProperty(thisArg, 'response', { value: textout });
- Object.defineProperty(thisArg, 'responseText', { value: textout });
- return;
- }
- });
- return Reflect.apply(target, thisArg, args);
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'm3u-prune.js',
- fn: m3uPrune,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-// https://en.wikipedia.org/wiki/M3U
-function m3uPrune(
- m3uPattern = '',
- urlPattern = ''
-) {
- if ( typeof m3uPattern !== 'string' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('m3u-prune', m3uPattern, urlPattern);
- const toLog = [];
- const regexFromArg = arg => {
- if ( arg === '' ) { return /^/; }
- const match = /^\/(.+)\/([gms]*)$/.exec(arg);
- if ( match !== null ) {
- let flags = match[2] || '';
- if ( flags.includes('m') ) { flags += 's'; }
- return new RegExp(match[1], flags);
- }
- return new RegExp(
- arg.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*+/g, '.*?')
- );
- };
- const reM3u = regexFromArg(m3uPattern);
- const reUrl = regexFromArg(urlPattern);
- const pruneSpliceoutBlock = (lines, i) => {
- if ( lines[i].startsWith('#EXT-X-CUE:TYPE="SpliceOut"') === false ) {
- return false;
- }
- toLog.push(`\t${lines[i]}`);
- lines[i] = undefined; i += 1;
- if ( lines[i].startsWith('#EXT-X-ASSET:CAID') ) {
- toLog.push(`\t${lines[i]}`);
- lines[i] = undefined; i += 1;
- }
- if ( lines[i].startsWith('#EXT-X-SCTE35:') ) {
- toLog.push(`\t${lines[i]}`);
- lines[i] = undefined; i += 1;
- }
- if ( lines[i].startsWith('#EXT-X-CUE-IN') ) {
- toLog.push(`\t${lines[i]}`);
- lines[i] = undefined; i += 1;
- }
- if ( lines[i].startsWith('#EXT-X-SCTE35:') ) {
- toLog.push(`\t${lines[i]}`);
- lines[i] = undefined; i += 1;
- }
- return true;
- };
- const pruneInfBlock = (lines, i) => {
- if ( lines[i].startsWith('#EXTINF') === false ) { return false; }
- if ( reM3u.test(lines[i+1]) === false ) { return false; }
- toLog.push('Discarding', `\t${lines[i]}, \t${lines[i+1]}`);
- lines[i] = lines[i+1] = undefined; i += 2;
- if ( lines[i].startsWith('#EXT-X-DISCONTINUITY') ) {
- toLog.push(`\t${lines[i]}`);
- lines[i] = undefined; i += 1;
- }
- return true;
- };
- const pruner = text => {
- if ( (/^\s*#EXTM3U/.test(text)) === false ) { return text; }
- if ( m3uPattern === '' ) {
- safe.uboLog(` Content:\n${text}`);
- return text;
- }
- if ( reM3u.multiline ) {
- reM3u.lastIndex = 0;
- for (;;) {
- const match = reM3u.exec(text);
- if ( match === null ) { break; }
- let discard = match[0];
- let before = text.slice(0, match.index);
- if (
- /^[\n\r]+/.test(discard) === false &&
- /[\n\r]+$/.test(before) === false
- ) {
- const startOfLine = /[^\n\r]+$/.exec(before);
- if ( startOfLine !== null ) {
- before = before.slice(0, startOfLine.index);
- discard = startOfLine[0] + discard;
- }
- }
- let after = text.slice(match.index + match[0].length);
- if (
- /[\n\r]+$/.test(discard) === false &&
- /^[\n\r]+/.test(after) === false
- ) {
- const endOfLine = /^[^\n\r]+/.exec(after);
- if ( endOfLine !== null ) {
- after = after.slice(endOfLine.index);
- discard += discard + endOfLine[0];
- }
- }
- text = before.trim() + '\n' + after.trim();
- reM3u.lastIndex = before.length + 1;
- toLog.push('Discarding', ...discard.split(/\n+/).map(s => `\t${s}`));
- if ( reM3u.global === false ) { break; }
- }
- return text;
- }
- const lines = text.split(/\n\r|\n|\r/);
- for ( let i = 0; i < lines.length; i++ ) {
- if ( lines[i] === undefined ) { continue; }
- if ( pruneSpliceoutBlock(lines, i) ) { continue; }
- if ( pruneInfBlock(lines, i) ) { continue; }
- }
- return lines.filter(l => l !== undefined).join('\n');
- };
- const urlFromArg = arg => {
- if ( typeof arg === 'string' ) { return arg; }
- if ( arg instanceof Request ) { return arg.url; }
- return String(arg);
- };
- const realFetch = self.fetch;
- self.fetch = new Proxy(self.fetch, {
- apply: function(target, thisArg, args) {
- if ( reUrl.test(urlFromArg(args[0])) === false ) {
- return Reflect.apply(target, thisArg, args);
- }
- return realFetch(...args).then(realResponse =>
- realResponse.text().then(text => {
- const response = new Response(pruner(text), {
- status: realResponse.status,
- statusText: realResponse.statusText,
- headers: realResponse.headers,
- });
- if ( toLog.length !== 0 ) {
- toLog.unshift(logPrefix);
- safe.uboLog(toLog.join('\n'));
- }
- return response;
- })
- );
- }
- });
- self.XMLHttpRequest.prototype.open = new Proxy(self.XMLHttpRequest.prototype.open, {
- apply: async (target, thisArg, args) => {
- if ( reUrl.test(urlFromArg(args[1])) === false ) {
- return Reflect.apply(target, thisArg, args);
- }
- thisArg.addEventListener('readystatechange', function() {
- if ( thisArg.readyState !== 4 ) { return; }
- const type = thisArg.responseType;
- if ( type !== '' && type !== 'text' ) { return; }
- const textin = thisArg.responseText;
- const textout = pruner(textin);
- if ( textout === textin ) { return; }
- Object.defineProperty(thisArg, 'response', { value: textout });
- Object.defineProperty(thisArg, 'responseText', { value: textout });
- if ( toLog.length !== 0 ) {
- toLog.unshift(logPrefix);
- safe.uboLog(toLog.join('\n'));
- }
- });
- return Reflect.apply(target, thisArg, args);
- }
- });
-}
-
-/*******************************************************************************
- *
- * @scriptlet href-sanitizer
- *
- * @description
- * Set the `href` attribute to a value found in the DOM at, or below the
- * targeted `a` element.
- *
- * ### Syntax
- *
- * ```text
- * example.org##+js(href-sanitizer, selector [, source])
- * ```
- *
- * - `selector`: required, CSS selector, specifies `a` elements for which the
- * `href` attribute must be overridden.
- * - `source`: optional, default to `text`, specifies from where to get the
- * value which will override the `href` attribute.
- * - `text`: the value will be the first valid URL found in the text
- * content of the targeted `a` element.
- * - `[attr]`: the value will be the attribute _attr_ of the targeted `a`
- * element.
- * - `?param`: the value will be the query parameter _param_ of the URL
- * found in the `href` attribute of the targeted `a` element.
- *
- * ### Examples
- *
- * example.org##+js(href-sanitizer, a)
- * example.org##+js(href-sanitizer, a[title], [title])
- * example.org##+js(href-sanitizer, a[href*="/away.php?to="], ?to)
- *
- * */
-
-builtinScriptlets.push({
- name: 'href-sanitizer.js',
- fn: hrefSanitizer,
- world: 'ISOLATED',
- dependencies: [
- 'run-at.fn',
- 'safe-self.fn',
- ],
-});
-function hrefSanitizer(
- selector = '',
- source = ''
-) {
- if ( typeof selector !== 'string' ) { return; }
- if ( selector === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('href-sanitizer', selector, source);
- if ( source === '' ) { source = 'text'; }
- const sanitizeCopycats = (href, text) => {
- let elems = [];
- try {
- elems = document.querySelectorAll(`a[href="${href}"`);
- }
- catch(ex) {
- }
- for ( const elem of elems ) {
- elem.setAttribute('href', text);
- }
- return elems.length;
- };
- const validateURL = text => {
- if ( text === '' ) { return ''; }
- if ( /[^\x21-\x7e]/.test(text) ) { return ''; }
- try {
- const url = new URL(text, document.location);
- return url.href;
- } catch(ex) {
- }
- return '';
- };
- const extractText = (elem, source) => {
- if ( /^\[.*\]$/.test(source) ) {
- return elem.getAttribute(source.slice(1,-1).trim()) || '';
- }
- if ( source.startsWith('?') ) {
- try {
- const url = new URL(elem.href, document.location);
- return url.searchParams.get(source.slice(1)) || '';
- } catch(x) {
- }
- return '';
- }
- if ( source === 'text' ) {
- return elem.textContent
- .replace(/^[^\x21-\x7e]+/, '') // remove leading invalid characters
- .replace(/[^\x21-\x7e]+$/, '') // remove trailing invalid characters
- ;
- }
- return '';
- };
- const sanitize = ( ) => {
- let elems = [];
- try {
- elems = document.querySelectorAll(selector);
- }
- catch(ex) {
- return false;
- }
- for ( const elem of elems ) {
- if ( elem.localName !== 'a' ) { continue; }
- if ( elem.hasAttribute('href') === false ) { continue; }
- const href = elem.getAttribute('href');
- const text = extractText(elem, source);
- const hrefAfter = validateURL(text);
- if ( hrefAfter === '' ) { continue; }
- if ( hrefAfter === href ) { continue; }
- elem.setAttribute('href', hrefAfter);
- const count = sanitizeCopycats(href, hrefAfter);
- safe.uboLog(logPrefix, `Sanitized ${count+1} links to\n${hrefAfter}`);
- }
- return true;
- };
- let observer, timer;
- const onDomChanged = mutations => {
- if ( timer !== undefined ) { return; }
- let shouldSanitize = false;
- for ( const mutation of mutations ) {
- if ( mutation.addedNodes.length === 0 ) { continue; }
- for ( const node of mutation.addedNodes ) {
- if ( node.nodeType !== 1 ) { continue; }
- shouldSanitize = true;
- break;
- }
- if ( shouldSanitize ) { break; }
- }
- if ( shouldSanitize === false ) { return; }
- timer = safe.onIdle(( ) => {
- timer = undefined;
- sanitize();
- });
- };
- const start = ( ) => {
- if ( sanitize() === false ) { return; }
- observer = new MutationObserver(onDomChanged);
- observer.observe(document.body, {
- subtree: true,
- childList: true,
- });
- };
- runAt(( ) => { start(); }, 'interactive');
-}
-
-/*******************************************************************************
- *
- * @scriptlet call-nothrow
- *
- * @description
- * Prevent a function call from throwing. The function will be called, however
- * should it throw, the scriptlet will silently process the exception and
- * returns as if no exception has occurred.
- *
- * ### Syntax
- *
- * ```text
- * example.org##+js(call-nothrow, propertyChain)
- * ```
- *
- * - `propertyChain`: a chain of dot-separated properties which leads to the
- * function to be trapped.
- *
- * ### Examples
- *
- * example.org##+js(call-nothrow, Object.defineProperty)
- *
- * */
-
-builtinScriptlets.push({
- name: 'call-nothrow.js',
- fn: callNothrow,
-});
-function callNothrow(
- chain = ''
-) {
- if ( typeof chain !== 'string' ) { return; }
- if ( chain === '' ) { return; }
- const parts = chain.split('.');
- let owner = window, prop;
- for (;;) {
- prop = parts.shift();
- if ( parts.length === 0 ) { break; }
- owner = owner[prop];
- if ( owner instanceof Object === false ) { return; }
- }
- if ( prop === '' ) { return; }
- const fn = owner[prop];
- if ( typeof fn !== 'function' ) { return; }
- owner[prop] = new Proxy(fn, {
- apply: function(...args) {
- let r;
- try {
- r = Reflect.apply(...args);
- } catch(ex) {
- }
- return r;
- },
- });
-}
-
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'spoof-css.js',
- fn: spoofCSS,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function spoofCSS(
- selector,
- ...args
-) {
- if ( typeof selector !== 'string' ) { return; }
- if ( selector === '' ) { return; }
- const toCamelCase = s => s.replace(/-[a-z]/g, s => s.charAt(1).toUpperCase());
- const propToValueMap = new Map();
- for ( let i = 0; i < args.length; i += 2 ) {
- if ( typeof args[i+0] !== 'string' ) { break; }
- if ( args[i+0] === '' ) { break; }
- if ( typeof args[i+1] !== 'string' ) { break; }
- propToValueMap.set(toCamelCase(args[i+0]), args[i+1]);
- }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('spoof-css', selector, ...args);
- const canDebug = scriptletGlobals.canDebug;
- const shouldDebug = canDebug && propToValueMap.get('debug') || 0;
- const instanceProperties = [ 'cssText', 'length', 'parentRule' ];
- const spoofStyle = (prop, real) => {
- const normalProp = toCamelCase(prop);
- const shouldSpoof = propToValueMap.has(normalProp);
- const value = shouldSpoof ? propToValueMap.get(normalProp) : real;
- if ( shouldSpoof ) {
- safe.uboLog(logPrefix, `Spoofing ${prop} to ${value}`);
- }
- return value;
- };
- const cloackFunc = (fn, thisArg, name) => {
- const trap = fn.bind(thisArg);
- Object.defineProperty(trap, 'name', { value: name });
- Object.defineProperty(trap, 'toString', {
- value: ( ) => `function ${name}() { [native code] }`
- });
- return trap;
- };
- self.getComputedStyle = new Proxy(self.getComputedStyle, {
- apply: function(target, thisArg, args) {
- // eslint-disable-next-line no-debugger
- if ( shouldDebug !== 0 ) { debugger; }
- const style = Reflect.apply(target, thisArg, args);
- const targetElements = new WeakSet(document.querySelectorAll(selector));
- if ( targetElements.has(args[0]) === false ) { return style; }
- const proxiedStyle = new Proxy(style, {
- get(target, prop, receiver) {
- if ( typeof target[prop] === 'function' ) {
- if ( prop === 'getPropertyValue' ) {
- return cloackFunc(function getPropertyValue(prop) {
- return spoofStyle(prop, target[prop]);
- }, target, 'getPropertyValue');
- }
- return cloackFunc(target[prop], target, prop);
- }
- if ( instanceProperties.includes(prop) ) {
- return Reflect.get(target, prop);
- }
- return spoofStyle(prop, Reflect.get(target, prop, receiver));
- },
- getOwnPropertyDescriptor(target, prop) {
- if ( propToValueMap.has(prop) ) {
- return {
- configurable: true,
- enumerable: true,
- value: propToValueMap.get(prop),
- writable: true,
- };
- }
- return Reflect.getOwnPropertyDescriptor(target, prop);
- },
- });
- return proxiedStyle;
- },
- get(target, prop, receiver) {
- if ( prop === 'toString' ) {
- return target.toString.bind(target);
- }
- return Reflect.get(target, prop, receiver);
- },
- });
- Element.prototype.getBoundingClientRect = new Proxy(Element.prototype.getBoundingClientRect, {
- apply: function(target, thisArg, args) {
- // eslint-disable-next-line no-debugger
- if ( shouldDebug !== 0 ) { debugger; }
- const rect = Reflect.apply(target, thisArg, args);
- const targetElements = new WeakSet(document.querySelectorAll(selector));
- if ( targetElements.has(thisArg) === false ) { return rect; }
- let { height, width } = rect;
- if ( propToValueMap.has('width') ) {
- width = parseFloat(propToValueMap.get('width'));
- }
- if ( propToValueMap.has('height') ) {
- height = parseFloat(propToValueMap.get('height'));
- }
- return new self.DOMRect(rect.x, rect.y, width, height);
- },
- get(target, prop, receiver) {
- if ( prop === 'toString' ) {
- return target.toString.bind(target);
- }
- return Reflect.get(target, prop, receiver);
- },
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'remove-node-text.js',
- aliases: [
- 'rmnt.js',
- ],
- fn: removeNodeText,
- world: 'ISOLATED',
- dependencies: [
- 'replace-node-text.fn',
- ],
-});
-function removeNodeText(
- nodeName,
- condition,
- ...extraArgs
-) {
- replaceNodeTextFn(nodeName, '', '', 'condition', condition || '', ...extraArgs);
-}
-
-/*******************************************************************************
- *
- * set-cookie.js
- *
- * Set specified cookie to a specific value.
- *
- * Reference:
- * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-cookie.js
- *
- **/
-
-builtinScriptlets.push({
- name: 'set-cookie.js',
- fn: setCookie,
- world: 'ISOLATED',
- dependencies: [
- 'safe-self.fn',
- 'set-cookie.fn',
- ],
-});
-function setCookie(
- name = '',
- value = '',
- path = ''
-) {
- if ( name === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path);
-
- const validValues = [
- 'accept', 'reject',
- 'accepted', 'rejected', 'notaccepted',
- 'allow', 'deny',
- 'allowed', 'disallow',
- 'enable', 'disable',
- 'enabled', 'disabled',
- 'ok',
- 'on', 'off',
- 'true', 't', 'false', 'f',
- 'yes', 'y', 'no', 'n',
- 'necessary', 'required',
- 'approved', 'disapproved',
- ];
- const normalized = value.toLowerCase();
- const match = /^("?)(.+)\1$/.exec(normalized);
- const unquoted = match && match[2] || normalized;
- if ( validValues.includes(unquoted) === false ) {
- if ( /^\d+$/.test(unquoted) === false ) { return; }
- const n = parseInt(value, 10);
- if ( n > 32767 ) { return; }
- }
-
- const done = setCookieFn(
- false,
- name,
- value,
- '',
- path,
- safe.getExtraArgs(Array.from(arguments), 3)
- );
-
- if ( done ) {
- safe.uboLog(logPrefix, 'Done');
- }
-}
-
-// For compatibility with AdGuard
-builtinScriptlets.push({
- name: 'set-cookie-reload.js',
- fn: setCookieReload,
- world: 'ISOLATED',
- dependencies: [
- 'set-cookie.js',
- ],
-});
-function setCookieReload(name, value, path, ...args) {
- setCookie(name, value, path, 'reload', '1', ...args);
-}
-
-/*******************************************************************************
- *
- * set-local-storage-item.js
- * set-session-storage-item.js
- *
- * Set a local/session storage entry to a specific, allowed value.
- *
- * Reference:
- * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-local-storage-item.js
- * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-session-storage-item.js
- *
- **/
-
-builtinScriptlets.push({
- name: 'set-local-storage-item.js',
- fn: setLocalStorageItem,
- world: 'ISOLATED',
- dependencies: [
- 'set-local-storage-item.fn',
- ],
-});
-function setLocalStorageItem(key = '', value = '') {
- setLocalStorageItemFn('local', false, key, value);
-}
-
-builtinScriptlets.push({
- name: 'set-session-storage-item.js',
- fn: setSessionStorageItem,
- world: 'ISOLATED',
- dependencies: [
- 'set-local-storage-item.fn',
- ],
-});
-function setSessionStorageItem(key = '', value = '') {
- setLocalStorageItemFn('session', false, key, value);
-}
-
-/*******************************************************************************
- *
- * @scriptlet set-attr
- *
- * @description
- * Sets the specified attribute on the specified elements. This scriptlet runs
- * once when the page loads then afterward on DOM mutations.
-
- * Reference: https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/set-attr.js
- *
- * ### Syntax
- *
- * ```text
- * example.org##+js(set-attr, selector, attr [, value])
- * ```
- *
- * - `selector`: CSS selector of DOM elements for which the attribute `attr`
- * must be modified.
- * - `attr`: the name of the attribute to modify
- * - `value`: the value to assign to the target attribute. Possible values:
- * - `''`: empty string (default)
- * - `true`
- * - `false`
- * - positive decimal integer 0 <= value < 32768
- * - `[other]`: copy the value from attribute `other` on the same element
- * */
-
-builtinScriptlets.push({
- name: 'set-attr.js',
- fn: setAttr,
- world: 'ISOLATED',
- dependencies: [
- 'run-at.fn',
- 'safe-self.fn',
- ],
-});
-function setAttr(
- selector = '',
- attr = '',
- value = ''
-) {
- if ( selector === '' ) { return; }
- if ( attr === '' ) { return; }
-
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('set-attr', attr, value);
- const validValues = [ '', 'false', 'true' ];
- let copyFrom = '';
-
- if ( validValues.includes(value.toLowerCase()) === false ) {
- if ( /^\d+$/.test(value) ) {
- const n = parseInt(value, 10);
- if ( n >= 32768 ) { return; }
- value = `${n}`;
- } else if ( /^\[.+\]$/.test(value) ) {
- copyFrom = value.slice(1, -1);
- } else {
- return;
- }
- }
-
- const extractValue = elem => {
- if ( copyFrom !== '' ) {
- return elem.getAttribute(copyFrom) || '';
- }
- return value;
- };
-
- const applySetAttr = ( ) => {
- const elems = [];
- try {
- elems.push(...document.querySelectorAll(selector));
- }
- catch(ex) {
- return false;
- }
- for ( const elem of elems ) {
- const before = elem.getAttribute(attr);
- const after = extractValue(elem);
- if ( after === before ) { continue; }
- if ( after !== '' && /^on/i.test(attr) ) {
- if ( attr.toLowerCase() in elem ) { continue; }
- }
- elem.setAttribute(attr, after);
- safe.uboLog(logPrefix, `${attr}="${after}"`);
- }
- return true;
- };
- let observer, timer;
- const onDomChanged = mutations => {
- if ( timer !== undefined ) { return; }
- let shouldWork = false;
- for ( const mutation of mutations ) {
- if ( mutation.addedNodes.length === 0 ) { continue; }
- for ( const node of mutation.addedNodes ) {
- if ( node.nodeType !== 1 ) { continue; }
- shouldWork = true;
- break;
- }
- if ( shouldWork ) { break; }
- }
- if ( shouldWork === false ) { return; }
- timer = self.requestAnimationFrame(( ) => {
- timer = undefined;
- applySetAttr();
- });
- };
- const start = ( ) => {
- if ( applySetAttr() === false ) { return; }
- observer = new MutationObserver(onDomChanged);
- observer.observe(document.body, {
- subtree: true,
- childList: true,
- });
- };
- runAt(( ) => { start(); }, 'idle');
-}
-
-/*******************************************************************************
- *
- * @scriptlet prevent-canvas
- *
- * @description
- * Prevent usage of specific or all (default) canvas APIs.
- *
- * ### Syntax
- *
- * ```text
- * example.com##+js(prevent-canvas [, contextType])
- * ```
- *
- * - `contextType`: A specific type of canvas API to prevent (default to all
- * APIs). Can be a string or regex which will be matched against the type
- * used in getContext() call. Prepend with `!` to test for no-match.
- *
- * ### Examples
- *
- * 1. Prevent `example.com` from accessing all canvas APIs
- *
- * ```adblock
- * example.com##+js(prevent-canvas)
- * ```
- *
- * 2. Prevent access to any flavor of WebGL API, everywhere
- *
- * ```adblock
- * *##+js(prevent-canvas, /webgl/)
- * ```
- *
- * 3. Prevent `example.com` from accessing any flavor of canvas API except `2d`
- *
- * ```adblock
- * example.com##+js(prevent-canvas, !2d)
- * ```
- *
- * ### References
- *
- * https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
- *
- * */
-
-builtinScriptlets.push({
- name: 'prevent-canvas.js',
- fn: preventCanvas,
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function preventCanvas(
- contextType = ''
-) {
- const safe = safeSelf();
- const pattern = safe.initPattern(contextType, { canNegate: true });
- const proto = globalThis.HTMLCanvasElement.prototype;
- proto.getContext = new Proxy(proto.getContext, {
- apply(target, thisArg, args) {
- if ( safe.testPattern(pattern, args[0]) ) { return null; }
- return Reflect.apply(target, thisArg, args);
- }
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'multiup.js',
- fn: multiup,
- world: 'ISOLATED',
-});
-function multiup() {
- const handler = ev => {
- const target = ev.target;
- if ( target.matches('button[link]') === false ) { return; }
- const ancestor = target.closest('form');
- if ( ancestor === null ) { return; }
- if ( ancestor !== target.parentElement ) { return; }
- const link = (target.getAttribute('link') || '').trim();
- if ( link === '' ) { return; }
- ev.preventDefault();
- ev.stopPropagation();
- document.location.href = link;
- };
- document.addEventListener('click', handler, { capture: true });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'remove-cache-storage-item.js',
- fn: removeCacheStorageItem,
- world: 'ISOLATED',
- dependencies: [
- 'safe-self.fn',
- ],
-});
-function removeCacheStorageItem(
- cacheNamePattern = '',
- requestPattern = ''
-) {
- if ( cacheNamePattern === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('remove-cache-storage-item', cacheNamePattern, requestPattern);
- const cacheStorage = self.caches;
- if ( cacheStorage instanceof Object === false ) { return; }
- const reCache = safe.patternToRegex(cacheNamePattern, undefined, true);
- const reRequest = safe.patternToRegex(requestPattern, undefined, true);
- cacheStorage.keys().then(cacheNames => {
- for ( const cacheName of cacheNames ) {
- if ( reCache.test(cacheName) === false ) { continue; }
- if ( requestPattern === '' ) {
- cacheStorage.delete(cacheName).then(result => {
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Deleting ${cacheName}`);
- }
- if ( result !== true ) { return; }
- safe.uboLog(logPrefix, `Deleted ${cacheName}: ${result}`);
- });
- continue;
- }
- cacheStorage.open(cacheName).then(cache => {
- cache.keys().then(requests => {
- for ( const request of requests ) {
- if ( reRequest.test(request.url) === false ) { continue; }
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Deleting ${cacheName}/${request.url}`);
- }
- cache.delete(request).then(result => {
- if ( result !== true ) { return; }
- safe.uboLog(logPrefix, `Deleted ${cacheName}/${request.url}: ${result}`);
- });
- }
- });
- });
- }
- });
-}
-
-
-/*******************************************************************************
- *
- * Scriplets below this section are only available for filter lists from
- * trusted sources. They all have the property `requiresTrust` set to `true`.
- *
- * Trusted sources are:
- *
- * - uBO's own filter lists, which name starts with "uBlock filters – ", and
- * maintained at: https://github.com/uBlockOrigin/uAssets
- *
- * - The user's own filters as seen in "My filters" pane in uBO's dashboard.
- *
- * The trustworthiness of filters using these privileged scriptlets are
- * evaluated at filter list compiled time: when a filter using one of the
- * privileged scriptlet originates from a non-trusted filter list source, it
- * is discarded at compile time, specifically from within:
- *
- * - Source: ./src/js/scriptlet-filtering.js
- * - Method: scriptletFilteringEngine.compile(), via normalizeRawFilter()
- *
- **/
-
-/*******************************************************************************
- *
- * replace-node-text.js
- *
- * Replace text instance(s) with another text instance inside specific
- * DOM nodes. By default, the scriplet stops and quits at the interactive
- * stage of a document.
- *
- * See commit messages for usage:
- * - https://github.com/gorhill/uBlock/commit/99ce027fd702
- * - https://github.com/gorhill/uBlock/commit/41876336db48
- *
- **/
-
-builtinScriptlets.push({
- name: 'trusted-replace-node-text.js',
- requiresTrust: true,
- aliases: [
- 'trusted-rpnt.js',
- 'replace-node-text.js',
- 'rpnt.js',
- ],
- fn: replaceNodeText,
- world: 'ISOLATED',
- dependencies: [
- 'replace-node-text.fn',
- ],
-});
-function replaceNodeText(
- nodeName,
- pattern,
- replacement,
- ...extraArgs
-) {
- replaceNodeTextFn(nodeName, pattern, replacement, ...extraArgs);
-}
-
-/*******************************************************************************
- *
- * trusted-set-constant.js
- *
- * Set specified property to any value. This is essentially the same as
- * set-constant.js, but with no restriction as to which values can be used.
- *
- **/
-
-builtinScriptlets.push({
- name: 'trusted-set-constant.js',
- requiresTrust: true,
- aliases: [
- 'trusted-set.js',
- ],
- fn: trustedSetConstant,
- dependencies: [
- 'set-constant.fn'
- ],
-});
-function trustedSetConstant(
- ...args
-) {
- setConstantFn(true, ...args);
-}
-
-/*******************************************************************************
- *
- * trusted-set-cookie.js
- *
- * Set specified cookie to an arbitrary value.
- *
- * Reference:
- * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-set-cookie.js#L23
- *
- **/
-
-builtinScriptlets.push({
- name: 'trusted-set-cookie.js',
- requiresTrust: true,
- fn: trustedSetCookie,
- world: 'ISOLATED',
- dependencies: [
- 'safe-self.fn',
- 'set-cookie.fn',
- ],
-});
-function trustedSetCookie(
- name = '',
- value = '',
- offsetExpiresSec = '',
- path = ''
-) {
- if ( name === '' ) { return; }
-
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('set-cookie', name, value, path);
- const time = new Date();
-
- if ( value.includes('$now$') ) {
- value = value.replaceAll('$now$', time.getTime());
- }
- if ( value.includes('$currentDate$') ) {
- value = value.replaceAll('$currentDate$', time.toUTCString());
- }
-
- let expires = '';
- if ( offsetExpiresSec !== '' ) {
- if ( offsetExpiresSec === '1day' ) {
- time.setDate(time.getDate() + 1);
- } else if ( offsetExpiresSec === '1year' ) {
- time.setFullYear(time.getFullYear() + 1);
- } else {
- if ( /^\d+$/.test(offsetExpiresSec) === false ) { return; }
- time.setSeconds(time.getSeconds() + parseInt(offsetExpiresSec, 10));
- }
- expires = time.toUTCString();
- }
-
- const done = setCookieFn(
- true,
- name,
- value,
- expires,
- path,
- safeSelf().getExtraArgs(Array.from(arguments), 4)
- );
-
- if ( done ) {
- safe.uboLog(logPrefix, 'Done');
- }
-}
-
-// For compatibility with AdGuard
-builtinScriptlets.push({
- name: 'trusted-set-cookie-reload.js',
- requiresTrust: true,
- fn: trustedSetCookieReload,
- world: 'ISOLATED',
- dependencies: [
- 'trusted-set-cookie.js',
- ],
-});
-function trustedSetCookieReload(name, value, offsetExpiresSec, path, ...args) {
- trustedSetCookie(name, value, offsetExpiresSec, path, 'reload', '1', ...args);
-}
-
-/*******************************************************************************
- *
- * trusted-set-local-storage-item.js
- *
- * Set a local storage entry to an arbitrary value.
- *
- * Reference:
- * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-set-local-storage-item.js
- *
- **/
-
-builtinScriptlets.push({
- name: 'trusted-set-local-storage-item.js',
- requiresTrust: true,
- fn: trustedSetLocalStorageItem,
- world: 'ISOLATED',
- dependencies: [
- 'set-local-storage-item.fn',
- ],
-});
-function trustedSetLocalStorageItem(key = '', value = '') {
- setLocalStorageItemFn('local', true, key, value);
-}
-
-builtinScriptlets.push({
- name: 'trusted-set-session-storage-item.js',
- requiresTrust: true,
- fn: trustedSetSessionStorageItem,
- world: 'ISOLATED',
- dependencies: [
- 'set-local-storage-item.fn',
- ],
-});
-function trustedSetSessionStorageItem(key = '', value = '') {
- setLocalStorageItemFn('session', true, key, value);
-}
-
-/*******************************************************************************
- *
- * trusted-replace-fetch-response.js
- *
- * Replaces response text content of fetch requests if all given parameters
- * match.
- *
- * Reference:
- * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-replace-fetch-response.js
- *
- **/
-
-builtinScriptlets.push({
- name: 'trusted-replace-fetch-response.js',
- requiresTrust: true,
- aliases: [
- 'trusted-rpfr.js',
- ],
- fn: trustedReplaceFetchResponse,
- dependencies: [
- 'replace-fetch-response.fn',
- ],
-});
-function trustedReplaceFetchResponse(...args) {
- replaceFetchResponseFn(true, ...args);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'trusted-replace-xhr-response.js',
- requiresTrust: true,
- fn: trustedReplaceXhrResponse,
- dependencies: [
- 'match-object-properties.fn',
- 'parse-properties-to-match.fn',
- 'safe-self.fn',
- ],
-});
-function trustedReplaceXhrResponse(
- pattern = '',
- replacement = '',
- propsToMatch = ''
-) {
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('trusted-replace-xhr-response', pattern, replacement, propsToMatch);
- const xhrInstances = new WeakMap();
- if ( pattern === '*' ) { pattern = '.*'; }
- const rePattern = safe.patternToRegex(pattern);
- const propNeedles = parsePropertiesToMatch(propsToMatch, 'url');
- self.XMLHttpRequest = class extends self.XMLHttpRequest {
- open(method, url, ...args) {
- const outerXhr = this;
- const xhrDetails = { method, url };
- let outcome = 'match';
- if ( propNeedles.size !== 0 ) {
- if ( matchObjectProperties(propNeedles, xhrDetails) === false ) {
- outcome = 'nomatch';
- }
- }
- if ( outcome === 'match' ) {
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, `Matched "propsToMatch"`);
- }
- xhrInstances.set(outerXhr, xhrDetails);
- }
- return super.open(method, url, ...args);
- }
- get response() {
- const innerResponse = super.response;
- const xhrDetails = xhrInstances.get(this);
- if ( xhrDetails === undefined ) {
- return innerResponse;
- }
- const responseLength = typeof innerResponse === 'string'
- ? innerResponse.length
- : undefined;
- if ( xhrDetails.lastResponseLength !== responseLength ) {
- xhrDetails.response = undefined;
- xhrDetails.lastResponseLength = responseLength;
- }
- if ( xhrDetails.response !== undefined ) {
- return xhrDetails.response;
- }
- if ( typeof innerResponse !== 'string' ) {
- return (xhrDetails.response = innerResponse);
- }
- const textBefore = innerResponse;
- const textAfter = textBefore.replace(rePattern, replacement);
- if ( textAfter !== textBefore ) {
- safe.uboLog(logPrefix, 'Match');
- }
- return (xhrDetails.response = textAfter);
- }
- get responseText() {
- const response = this.response;
- if ( typeof response !== 'string' ) {
- return super.responseText;
- }
- return response;
- }
- };
-}
-
-/*******************************************************************************
- *
- * trusted-click-element.js
- *
- * Reference API:
- * https://github.com/AdguardTeam/Scriptlets/blob/master/src/scriptlets/trusted-click-element.js
- *
- **/
-
-builtinScriptlets.push({
- name: 'trusted-click-element.js',
- requiresTrust: true,
- fn: trustedClickElement,
- world: 'ISOLATED',
- dependencies: [
- 'get-all-cookies.fn',
- 'get-all-local-storage.fn',
- 'run-at-html-element.fn',
- 'safe-self.fn',
- ],
-});
-function trustedClickElement(
- selectors = '',
- extraMatch = '',
- delay = ''
-) {
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('trusted-click-element', selectors, extraMatch, delay);
-
- if ( extraMatch !== '' ) {
- const assertions = extraMatch.split(',').map(s => {
- const pos1 = s.indexOf(':');
- const s1 = pos1 !== -1 ? s.slice(0, pos1) : s;
- const not = s1.startsWith('!');
- const type = not ? s1.slice(1) : s1;
- const s2 = pos1 !== -1 ? s.slice(pos1+1).trim() : '';
- if ( s2 === '' ) { return; }
- const out = { not, type };
- const match = /^\/(.+)\/(i?)$/.exec(s2);
- if ( match !== null ) {
- out.re = new RegExp(match[1], match[2] || undefined);
- return out;
- }
- const pos2 = s2.indexOf('=');
- const key = pos2 !== -1 ? s2.slice(0, pos2).trim() : s2;
- const value = pos2 !== -1 ? s2.slice(pos2+1).trim() : '';
- out.re = new RegExp(`^${this.escapeRegexChars(key)}=${this.escapeRegexChars(value)}`);
- return out;
- }).filter(details => details !== undefined);
- const allCookies = assertions.some(o => o.type === 'cookie')
- ? getAllCookiesFn()
- : [];
- const allStorageItems = assertions.some(o => o.type === 'localStorage')
- ? getAllLocalStorageFn()
- : [];
- const hasNeedle = (haystack, needle) => {
- for ( const { key, value } of haystack ) {
- if ( needle.test(`${key}=${value}`) ) { return true; }
- }
- return false;
- };
- for ( const { not, type, re } of assertions ) {
- switch ( type ) {
- case 'cookie':
- if ( hasNeedle(allCookies, re) === not ) { return; }
- break;
- case 'localStorage':
- if ( hasNeedle(allStorageItems, re) === not ) { return; }
- break;
- }
- }
- }
-
- const getShadowRoot = elem => {
- // Firefox
- if ( elem.openOrClosedShadowRoot ) {
- return elem.openOrClosedShadowRoot;
- }
- // Chromium
- if ( typeof chrome === 'object' ) {
- if ( chrome.dom && chrome.dom.openOrClosedShadowRoot ) {
- return chrome.dom.openOrClosedShadowRoot(elem);
- }
- }
- return null;
- };
-
- const querySelectorEx = (selector, context = document) => {
- const pos = selector.indexOf(' >>> ');
- if ( pos === -1 ) { return context.querySelector(selector); }
- const outside = selector.slice(0, pos).trim();
- const inside = selector.slice(pos + 5).trim();
- const elem = context.querySelector(outside);
- if ( elem === null ) { return null; }
- const shadowRoot = getShadowRoot(elem);
- return shadowRoot && querySelectorEx(inside, shadowRoot);
- };
-
- const selectorList = selectors.split(/\s*,\s*/)
- .filter(s => {
- try {
- void querySelectorEx(s);
- } catch(_) {
- return false;
- }
- return true;
- });
- if ( selectorList.length === 0 ) { return; }
-
- const clickDelay = parseInt(delay, 10) || 1;
- const t0 = Date.now();
- const tbye = t0 + 10000;
- let tnext = selectorList.length !== 1 ? t0 : t0 + clickDelay;
-
- const terminate = ( ) => {
- selectorList.length = 0;
- next.stop();
- observe.stop();
- };
-
- const next = notFound => {
- if ( selectorList.length === 0 ) {
- safe.uboLog(logPrefix, 'Completed');
- return terminate();
- }
- const tnow = Date.now();
- if ( tnow >= tbye ) {
- safe.uboLog(logPrefix, 'Timed out');
- return terminate();
- }
- if ( notFound ) { observe(); }
- const delay = Math.max(notFound ? tbye - tnow : tnext - tnow, 1);
- next.timer = setTimeout(( ) => {
- next.timer = undefined;
- process();
- }, delay);
- safe.uboLog(logPrefix, `Waiting for ${selectorList[0]}...`);
- };
- next.stop = ( ) => {
- if ( next.timer === undefined ) { return; }
- clearTimeout(next.timer);
- next.timer = undefined;
- };
-
- const observe = ( ) => {
- if ( observe.observer !== undefined ) { return; }
- observe.observer = new MutationObserver(( ) => {
- if ( observe.timer !== undefined ) { return; }
- observe.timer = setTimeout(( ) => {
- observe.timer = undefined;
- process();
- }, 20);
- });
- observe.observer.observe(document, {
- attributes: true,
- childList: true,
- subtree: true,
- });
- };
- observe.stop = ( ) => {
- if ( observe.timer !== undefined ) {
- clearTimeout(observe.timer);
- observe.timer = undefined;
- }
- if ( observe.observer ) {
- observe.observer.disconnect();
- observe.observer = undefined;
- }
- };
-
- const process = ( ) => {
- next.stop();
- if ( Date.now() < tnext ) { return next(); }
- const selector = selectorList.shift();
- if ( selector === undefined ) { return terminate(); }
- const elem = querySelectorEx(selector);
- if ( elem === null ) {
- selectorList.unshift(selector);
- return next(true);
- }
- safe.uboLog(logPrefix, `Clicked ${selector}`);
- elem.click();
- tnext += clickDelay;
- next();
- };
-
- runAtHtmlElementFn(process);
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'trusted-prune-inbound-object.js',
- requiresTrust: true,
- fn: trustedPruneInboundObject,
- dependencies: [
- 'object-find-owner.fn',
- 'object-prune.fn',
- 'safe-self.fn',
- ],
-});
-function trustedPruneInboundObject(
- entryPoint = '',
- argPos = '',
- rawPrunePaths = '',
- rawNeedlePaths = ''
-) {
- if ( entryPoint === '' ) { return; }
- let context = globalThis;
- let prop = entryPoint;
- for (;;) {
- const pos = prop.indexOf('.');
- if ( pos === -1 ) { break; }
- context = context[prop.slice(0, pos)];
- if ( context instanceof Object === false ) { return; }
- prop = prop.slice(pos+1);
- }
- if ( typeof context[prop] !== 'function' ) { return; }
- const argIndex = parseInt(argPos);
- if ( isNaN(argIndex) ) { return; }
- if ( argIndex < 1 ) { return; }
- const safe = safeSelf();
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 4);
- const needlePaths = [];
- if ( rawPrunePaths !== '' ) {
- needlePaths.push(...rawPrunePaths.split(/ +/));
- }
- if ( rawNeedlePaths !== '' ) {
- needlePaths.push(...rawNeedlePaths.split(/ +/));
- }
- const stackNeedle = safe.initPattern(extraArgs.stackToMatch || '', { canNegate: true });
- const mustProcess = root => {
- for ( const needlePath of needlePaths ) {
- if ( objectFindOwnerFn(root, needlePath) === false ) {
- return false;
- }
- }
- return true;
- };
- context[prop] = new Proxy(context[prop], {
- apply: function(target, thisArg, args) {
- const targetArg = argIndex <= args.length
- ? args[argIndex-1]
- : undefined;
- if ( targetArg instanceof Object && mustProcess(targetArg) ) {
- let objBefore = targetArg;
- if ( extraArgs.dontOverwrite ) {
- try {
- objBefore = safe.JSON_parse(safe.JSON_stringify(targetArg));
- } catch(_) {
- objBefore = undefined;
- }
- }
- if ( objBefore !== undefined ) {
- const objAfter = objectPruneFn(
- objBefore,
- rawPrunePaths,
- rawNeedlePaths,
- stackNeedle,
- extraArgs
- );
- args[argIndex-1] = objAfter || objBefore;
- }
- }
- return Reflect.apply(target, thisArg, args);
- },
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'trusted-prune-outbound-object.js',
- requiresTrust: true,
- fn: trustedPruneOutboundObject,
- dependencies: [
- 'object-prune.fn',
- 'proxy-apply.fn',
- 'safe-self.fn',
- ],
-});
-function trustedPruneOutboundObject(
- propChain = '',
- rawPrunePaths = '',
- rawNeedlePaths = ''
-) {
- if ( propChain === '' ) { return; }
- const safe = safeSelf();
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
- const reflector = proxyApplyFn(propChain, function(...args) {
- const objBefore = reflector(...args);
- if ( objBefore instanceof Object === false ) { return objBefore; }
- const objAfter = objectPruneFn(
- objBefore,
- rawPrunePaths,
- rawNeedlePaths,
- { matchAll: true },
- extraArgs
- );
- return objAfter || objBefore;
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'trusted-replace-argument.js',
- requiresTrust: true,
- fn: trustedReplaceArgument,
- dependencies: [
- 'proxy-apply.fn',
- 'safe-self.fn',
- 'validate-constant.fn',
- ],
-});
-function trustedReplaceArgument(
- propChain = '',
- argpos = '',
- argraw = ''
-) {
- if ( propChain === '' ) { return; }
- if ( argpos === '' ) { return; }
- if ( argraw === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('trusted-replace-argument', propChain, argpos, argraw);
- const extraArgs = safe.getExtraArgs(Array.from(arguments), 3);
- const normalValue = validateConstantFn(true, argraw);
- const reCondition = extraArgs.condition
- ? safe.patternToRegex(extraArgs.condition)
- : /^/;
- const reflector = proxyApplyFn(propChain, function(...args) {
- const arglist = args[args.length-1];
- if ( Array.isArray(arglist) === false ) { return reflector(...args); }
- const argBefore = arglist[argpos];
- if ( reCondition.test(argBefore) === false ) { return reflector(...args); }
- arglist[argpos] = normalValue;
- safe.uboLog(logPrefix, `Replaced argument:\nBefore: ${JSON.stringify(argBefore)}\nAfter: ${normalValue}`);
- return reflector(...args);
- });
-}
-
-/******************************************************************************/
-
-builtinScriptlets.push({
- name: 'trusted-replace-outbound-text.js',
- requiresTrust: true,
- fn: trustedReplaceOutboundText,
- dependencies: [
- 'proxy-apply.fn',
- 'safe-self.fn',
- ],
-});
-function trustedReplaceOutboundText(
- propChain = '',
- pattern = '',
- replacement = '',
- ...args
-) {
- if ( propChain === '' ) { return; }
- const safe = safeSelf();
- const logPrefix = safe.makeLogPrefix('trusted-replace-outbound-text', propChain, pattern, replacement, ...args);
- const rePattern = safe.patternToRegex(pattern);
- const extraArgs = safe.getExtraArgs(args);
- const reCondition = safe.patternToRegex(extraArgs.condition || '');
- const reflector = proxyApplyFn(propChain, function(...args) {
- const textBefore = reflector(...args);
- if ( pattern === '' ) {
- safe.uboLog(logPrefix, 'Outbound text:\n', textBefore);
- return textBefore;
- }
- reCondition.lastIndex = 0;
- if ( reCondition.test(textBefore) === false ) { return textBefore; }
- const textAfter = textBefore.replace(rePattern, replacement);
- if ( textAfter === textBefore ) { return textBefore; }
- safe.uboLog(logPrefix, 'Matched and replaced');
- if ( safe.logLevel > 1 ) {
- safe.uboLog(logPrefix, 'Modified outbound text:\n', textAfter);
- }
- return textAfter;
- });
-}
-
-/******************************************************************************/
\ No newline at end of file
+// Stable entry point used by build.rs and CI. See README.md for provenance.
+export { builtinScriptlets } from './scriptlet-resources/scriptlets.js';
diff --git a/privaxy/src/resources/vendor/ublock/urlskip.js b/privaxy/src/resources/vendor/ublock/urlskip.js
new file mode 100644
index 00000000..7ca8644a
--- /dev/null
+++ b/privaxy/src/resources/vendor/ublock/urlskip.js
@@ -0,0 +1,162 @@
+/*******************************************************************************
+
+ uBlock Origin - a comprehensive, efficient content blocker
+ Copyright (C) 2022-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+/**
+ * @trustedOption urlskip
+ *
+ * @description
+ * Extract a URL from another URL according to one or more transformation steps,
+ * thereby skipping over intermediate network request(s) to remote servers.
+ * Requires a trusted source.
+ *
+ * @param steps
+ * A serie of space-separated directives representing the transformation steps
+ * to perform to extract the final URL to which a network request should be
+ * redirected.
+ *
+ * Supported directives:
+ *
+ * `?name`: extract the value of parameter `name` as the current string.
+ *
+ * `&i`: extract the name of the parameter at position `i` as the current
+ * string. The position is 1-based.
+ *
+ * `#`: extract the hash as the current string.
+ *
+ * `/.../`: extract the first capture group of a regex as the current string.
+ *
+ * `+https`: prepend the current string with `https://`.
+ *
+ * `-base64`: decode the current string as a base64-encoded string.
+ *
+ * `-safebase64`: decode the current string as a safe base64-encoded string.
+ *
+ * `-uricomponent`: decode the current string as a URI encoded string.
+ *
+ * `-blocked`: allow the redirection of blocked requests. By default, blocked
+ * requests can't by urlskip'ed.
+ *
+ * At any given step, the currently extracted string may not necessarily be
+ * a valid URL, and more transformation steps may be needed to obtain a valid
+ * URL once all the steps are applied.
+ *
+ * An unsupported step or a failed step will abort the transformation and no
+ * redirection will be performed.
+ *
+ * The final step is expected to yield a valid URL. If the result is not a
+ * valid URL, no redirection will be performed.
+ *
+ * @example
+ * ||example.com/path/to/tracker$urlskip=?url
+ * ||example.com/path/to/tracker$urlskip=?url ?to
+ * ||pixiv.net/jump.php?$urlskip=&1
+ * ||podtrac.com/pts/redirect.mp3/$urlskip=/\/redirect\.mp3\/(.*?\.mp3\b)/ +https
+ *
+ * */
+
+export function urlSkip(url, blocked, steps) {
+ try {
+ let redirectBlocked = false;
+ let urlout = url;
+ for ( const step of steps ) {
+ const urlin = urlout;
+ const c0 = step.charCodeAt(0);
+ // Extract from hash
+ if ( c0 === 0x23 && step === '#' ) { // #
+ const pos = urlin.indexOf('#');
+ urlout = pos !== -1 ? urlin.slice(pos+1) : '';
+ continue;
+ }
+ // Extract from URL parameter name at position i
+ if ( c0 === 0x26 ) { // &
+ const i = (parseInt(step.slice(1)) || 0) - 1;
+ if ( i < 0 ) { return; }
+ const url = new URL(urlin);
+ if ( i >= url.searchParams.size ) { return; }
+ const params = Array.from(url.searchParams.keys());
+ urlout = decodeURIComponent(params[i]);
+ continue;
+ }
+ // Enforce https
+ if ( c0 === 0x2B && step === '+https' ) { // +
+ const s = urlin.replace(/^https?:\/\//, '');
+ if ( /^[\w-]:\/\//.test(s) ) { return; }
+ urlout = `https://${s}`;
+ continue;
+ }
+ // Decode
+ if ( c0 === 0x2D ) { // -
+ // Base64
+ if ( step === '-base64' ) {
+ urlout = self.atob(urlin);
+ continue;
+ }
+ // Safe Base64
+ if ( step === '-safebase64' ) {
+ if ( urlSkip.safeBase64Replacer === undefined ) {
+ urlSkip.safeBase64Map = { '-': '+', '_': '/' };
+ urlSkip.safeBase64Replacer = s => urlSkip.safeBase64Map[s];
+ }
+ urlout = urlin.replace(/[-_]/g, urlSkip.safeBase64Replacer);
+ urlout = self.atob(urlout);
+ continue;
+ }
+ // URI component
+ if ( step === '-uricomponent' ) {
+ urlout = decodeURIComponent(urlin);
+ continue;
+ }
+ // Enable skip of blocked requests
+ if ( step === '-blocked' ) {
+ redirectBlocked = true;
+ continue;
+ }
+ }
+ // Regex extraction from first capture group
+ if ( c0 === 0x2F ) { // /
+ const re = new RegExp(step.slice(1, -1));
+ const match = re.exec(urlin);
+ if ( match === null ) { return; }
+ if ( match.length <= 1 ) { return; }
+ urlout = match[1];
+ continue;
+ }
+ // Extract from URL parameter
+ if ( c0 === 0x3F ) { // ?
+ urlout = (new URL(urlin)).searchParams.get(step.slice(1));
+ if ( urlout === null ) { return; }
+ if ( urlout.includes(' ') ) {
+ urlout = urlout.replace(/ /g, '%20');
+ }
+ continue;
+ }
+ // Unknown directive
+ return;
+ }
+ const urlfinal = new URL(urlout);
+ if ( urlfinal.protocol !== 'https:' ) {
+ if ( urlfinal.protocol !== 'http:' ) { return; }
+ }
+ if ( blocked && redirectBlocked !== true ) { return; }
+ return urlout;
+ } catch {
+ }
+}
diff --git a/privaxy/src/server/blocker.rs b/privaxy/src/server/blocker.rs
index ab21b30c..ae53047a 100644
--- a/privaxy/src/server/blocker.rs
+++ b/privaxy/src/server/blocker.rs
@@ -1,11 +1,12 @@
use crate::blocker_utils::{build_resource_from_file_contents, read_redirectable_resource_mapping};
+use crate::filter_list::{preprocess, FilterList, TRUSTED_SCRIPTLETS};
use adblock::lists::FilterSet;
use adblock::request::Request;
use adblock::resources::Resource;
use adblock::Engine;
use include_dir::{include_dir, Dir};
use lazy_static::lazy_static;
-use std::collections::{HashMap, HashSet};
+use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
@@ -101,13 +102,74 @@ fn unmatched_result() -> adblock::blocker::BlockerResult {
/// Build an engine from raw filter lists. CPU-heavy — seconds for full-size
/// lists — so callers must not run it on an async task directly.
-fn build_engine(filters: Vec) -> Engine {
+fn build_engine(filters: Vec) -> Engine {
log::debug!("Configuring blocking engine.");
let mut filter_set = FilterSet::new(true);
for filter in filters {
- filter_set.add_filter_list(&filter, adblock::lists::ParseOptions::default());
+ let parsed = match preprocess(&filter.contents) {
+ Ok(parsed) => parsed,
+ Err(error) => {
+ log::warn!("Skipping filter list '{}': {error}", filter.name);
+ continue;
+ }
+ };
+ if parsed.unknown_conditions > 0 {
+ log::warn!(
+ "Filter list '{}': {} unknown conditions left unfiltered",
+ filter.name,
+ parsed.unknown_conditions
+ );
+ }
+ let options = adblock::lists::ParseOptions {
+ permissions: if filter.trusted {
+ TRUSTED_SCRIPTLETS
+ } else {
+ Default::default()
+ },
+ ..Default::default()
+ };
+ let mut supported = 0;
+ let mut unsupported = 0;
+ let mut rejection_reasons = BTreeMap::::new();
+ for line in parsed.lines {
+ if line.is_empty()
+ || line.starts_with('!')
+ || line.starts_with("[Adblock")
+ || line
+ .strip_prefix('#')
+ .is_some_and(|rest| rest.is_empty() || rest.starts_with(char::is_whitespace))
+ {
+ continue;
+ }
+ match filter_set.add_filter(line, options) {
+ Ok(()) => supported += 1,
+ Err(error) => {
+ unsupported += 1;
+ // Regex errors may contain the full rule. Keep diagnostics
+ // useful without logging custom URLs or query parameters.
+ let reason = match error {
+ adblock::lists::FilterParseError::Network(
+ adblock::filters::network::NetworkFilterError::RegexParsingError(_),
+ ) => "network filter error: invalid regular expression".to_owned(),
+ error => error.to_string(),
+ };
+ *rejection_reasons.entry(reason).or_default() += 1;
+ }
+ }
+ }
+ if unsupported > 0 {
+ let reasons = rejection_reasons
+ .iter()
+ .map(|(reason, count)| format!("{count} {reason}"))
+ .collect::>()
+ .join("; ");
+ log::warn!(
+ "Filter list '{}': loaded {supported} rules; skipped {unsupported} unsupported or invalid rules ({reasons})",
+ filter.name
+ );
+ }
}
let mut adblock_engine = Engine::from_filter_set(filter_set, true);
@@ -198,7 +260,7 @@ impl AdblockRequester {
self.engine.read().unwrap().clone()
}
- pub(crate) async fn replace_engine(&self, filters: Vec) {
+ pub(crate) async fn replace_engine(&self, filters: Vec) {
let new_engine = tokio::task::spawn_blocking(move || build_engine(filters))
.await
.expect("engine build task panicked");
@@ -271,6 +333,72 @@ impl AdblockRequester {
mod tests {
use super::*;
+ fn list(contents: &str) -> FilterList {
+ FilterList {
+ name: "Test list".to_owned(),
+ contents: contents.to_owned(),
+ trusted: false,
+ }
+ }
+
+ #[test]
+ fn conditional_blocks_affect_matching_and_do_not_leak_between_lists() {
+ let engine = build_engine(vec![
+ list("!#if cap_html_filtering\n||html.example^\n!#else\n||fallback.example^\n!#endif"),
+ list("||other.example^"),
+ ]);
+ for (host, expected) in [
+ ("html.example", false),
+ ("fallback.example", true),
+ ("other.example", true),
+ ] {
+ let request = Request::new(
+ &format!("https://{host}/ad.js"),
+ "https://site.example/",
+ "script",
+ )
+ .unwrap();
+ assert_eq!(engine.check_network_request(&request).matched, expected);
+ }
+ }
+
+ #[test]
+ fn modern_json_edit_scriptlets_include_their_shared_dependencies() {
+ let engine = build_engine(vec![list("m.youtube.com##+js(json-edit-fetch-response, ..itemSectionRenderer.contents.*[?.adSlotRenderer], propsToMatch, /get_watch)")]);
+ let result = cosmetic_response(&engine, "https://m.youtube.com/");
+ let script = result.injected_script.unwrap();
+ assert!(script.contains("function jsonEditFetchResponse("));
+ assert!(script.contains("class JSONPath"));
+ assert!(script.contains("function safeSelf("));
+ assert!(script.contains("jsonEditFetchResponse(\"..itemSectionRenderer.contents.*[?.adSlotRenderer]\", \"propsToMatch\", \"/get_watch\")"));
+ }
+
+ #[test]
+ fn trusted_scriptlets_require_trusted_list_permissions() {
+ let rules =
+ "example.com##+js(trusted-replace-fetch-response, adPlacements, no_ads, player?)";
+ let engine = build_engine(vec![list(rules)]);
+ assert!(cosmetic_response(&engine, "https://example.com/")
+ .injected_script
+ .is_none());
+ let mut trusted = list(rules);
+ trusted.trusted = true;
+ let engine = build_engine(vec![trusted]);
+ assert!(cosmetic_response(&engine, "https://example.com/")
+ .injected_script
+ .is_some());
+ }
+
+ #[test]
+ fn scriptlets_receive_more_than_nine_arguments_and_no_literal_placeholders() {
+ let engine = build_engine(vec![list("example.com##+js(json-prune-fetch-response, ads, , propsToMatch, player, log, 1, stackToMatch, test, extra, tenth, eleventh)")]);
+ let script = cosmetic_response(&engine, "https://example.com/")
+ .injected_script
+ .unwrap();
+ assert!(script.contains("\"tenth\", \"eleventh\")"));
+ assert!(!script.contains("{{1}}"));
+ }
+
fn requester() -> AdblockRequester {
AdblockRequester::new(BlockingDisabledStore(Arc::new(RwLock::new(false))))
}
@@ -291,7 +419,7 @@ mod tests {
async fn replace_engine_applies_network_filters() {
let requester = requester();
requester
- .replace_engine(vec![String::from("||ads.example.com^")])
+ .replace_engine(vec![list("||ads.example.com^")])
.await;
let (blocked, result) = requester.is_network_url_blocked(
@@ -316,7 +444,7 @@ mod tests {
async fn disabling_blocking_bypasses_matching() {
let requester = requester();
requester
- .replace_engine(vec![String::from("||ads.example.com^")])
+ .replace_engine(vec![list("||ads.example.com^")])
.await;
requester.blocking_disabled.set(false);
@@ -338,9 +466,7 @@ mod tests {
async fn cosmetic_lookup_splits_url_scoped_and_generic_selectors() {
let requester = requester();
requester
- .replace_engine(vec![String::from(
- "example.com###site-ad\n##.generic-ad-class",
- )])
+ .replace_engine(vec![list("example.com###site-ad\n##.generic-ad-class")])
.await;
// The URL-scoped lookup carries the specific selector but not the
diff --git a/privaxy/src/server/cert.rs b/privaxy/src/server/cert.rs
index 0acc1409..afb857bf 100644
--- a/privaxy/src/server/cert.rs
+++ b/privaxy/src/server/cert.rs
@@ -49,10 +49,13 @@ impl SignedWithCaCert {
// (ring) is installed once as the process default at startup.
let private_key =
PrivateKeyDer::try_from(private_key.private_key_to_der().unwrap()).unwrap();
- let server_configuration = ServerConfig::builder()
+ let mut server_configuration = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, private_key)
.unwrap();
+ // The CONNECT service supports both versions. Advertise HTTP/2 so
+ // browsers can multiplex page resources on one intercepted session.
+ server_configuration.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
Self {
authority,
@@ -218,6 +221,9 @@ impl CertCache {
cache.insert(certificate);
}
+ /// `authority` is the TLS identity (DNS SNI for an IP CONNECT), not
+ /// necessarily the socket destination. Cache and SAN generation must use
+ /// that same identity so virtual hosts sharing an IP cannot share a leaf.
pub async fn get(&self, authority: Authority) -> SignedWithCaCert {
let mut cache = self.cache.lock().await;
diff --git a/privaxy/src/server/configuration/filter.rs b/privaxy/src/server/configuration/filter.rs
index cde79aeb..c61773e1 100644
--- a/privaxy/src/server/configuration/filter.rs
+++ b/privaxy/src/server/configuration/filter.rs
@@ -6,6 +6,10 @@ use std::path::{Path, PathBuf};
use tokio::fs;
use url::Url;
+use crate::filter_list::{is_trusted_source, preprocess, FilterList};
+
+mod download;
+
use serde_with::{serde_as, DisplayFromStr};
pub(crate) const FILTERS_DIRECTORY_NAME: &str = "filters";
@@ -121,13 +125,13 @@ impl DefaultFilters {
fn get_default_filters() -> Vec {
vec![
- ("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt", "uBlock filters", FilterGroup::Default, true),
+ ("https://raw.githubusercontent.com/uBlockOrigin/uAssetsCDN/main/filters/filters.min.txt", "uBlock Origin filters - Ads", FilterGroup::Default, true),
("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-mobile.txt", "uBlock mobile filters", FilterGroup::Default, true),
- ("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/badware.txt", "uBlock filters - Badware risks", FilterGroup::Default, true),
- ("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/privacy.txt", "uBlock filters - Privacy", FilterGroup::Default, true),
+ ("https://raw.githubusercontent.com/uBlockOrigin/uAssetsCDN/main/filters/badware.min.txt", "uBlock Origin filters - Badware risks", FilterGroup::Default, true),
+ ("https://raw.githubusercontent.com/uBlockOrigin/uAssetsCDN/main/filters/privacy.min.txt", "uBlock Origin filters - Privacy", FilterGroup::Default, true),
("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/resource-abuse.txt", "uBlock filters - Resource abuse", FilterGroup::Default, true),
- ("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/unbreak.txt", "uBlock filters - Unbreak", FilterGroup::Default, true),
- ("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/quick-fixes.txt", "uBlock filters - Quick Fixes", FilterGroup::Default, true),
+ ("https://raw.githubusercontent.com/uBlockOrigin/uAssetsCDN/main/filters/unbreak.min.txt", "uBlock Origin filters - Unbreak", FilterGroup::Default, true),
+ ("https://raw.githubusercontent.com/uBlockOrigin/uAssetsCDN/main/filters/quick-fixes.min.txt", "uBlock Origin filters - Quick fixes", FilterGroup::Default, true),
]
.into_iter()
.filter_map(|(url, title, group, enabled_by_default)| Self::parse_filter(url, title, group, enabled_by_default))
@@ -259,6 +263,58 @@ pub(crate) fn calculate_sha256_hex(input: &str) -> String {
hex::encode(hasher.finalize())
}
+/// Upgrade only the five known source subscriptions. Existing CDN entries take
+/// precedence, including their enabled state; unrelated lists are untouched.
+/// A new filename forces a download of the complete bundle instead of reusing
+/// the old source fragment. Old cache files remain available for rollback.
+pub(crate) fn migrate_ublock_sources(filters: &mut Vec) -> bool {
+ let mut changed = false;
+ let defaults = DefaultFilters::new();
+ for name in ["filters", "badware", "privacy", "quick-fixes", "unbreak"] {
+ let legacy = format!(
+ "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/{name}.txt"
+ );
+ let legacy_ref = format!("https://raw.githubusercontent.com/uBlockOrigin/uAssets/refs/heads/master/filters/{name}.txt");
+ let canonical = format!(
+ "https://raw.githubusercontent.com/uBlockOrigin/uAssetsCDN/main/filters/{name}.min.txt"
+ );
+ let is_legacy =
+ |filter: &Filter| filter.url.as_str() == legacy || filter.url.as_str() == legacy_ref;
+ if !filters.iter().any(is_legacy) {
+ continue;
+ }
+ if filters
+ .iter()
+ .any(|filter| filter.url.as_str() == canonical)
+ {
+ filters.retain(|filter| !is_legacy(filter));
+ } else {
+ let replacement = defaults
+ .0
+ .iter()
+ .find(|filter| filter.url.as_str() == canonical)
+ .unwrap();
+ // Both spellings may be present. Keep the first entry's settings.
+ let mut migrated = false;
+ filters.retain_mut(|filter| {
+ if !is_legacy(filter) {
+ return true;
+ }
+ if migrated {
+ return false;
+ }
+ filter.url = replacement.url.clone();
+ filter.file_name = replacement.file_name.clone();
+ filter.title = replacement.title.clone();
+ migrated = true;
+ true
+ });
+ }
+ changed = true;
+ }
+ changed
+}
+
pub(crate) fn calc_filter_filename(filename: &str) -> String {
format!("{}.txt", calculate_sha256_hex(filename))
}
@@ -460,6 +516,9 @@ fn validate_filter_content_type(
/// Verifies at least one rule is present.
fn validate_filter_rules(filter: &Filter, contents: &str) -> super::ConfigurationResult<()> {
+ preprocess(contents).map_err(|error| {
+ super::ConfigurationError::FilterValidationError(format!("{}: {error}", filter.title))
+ })?;
// Validation also runs on cache hits. Stop at the first usable rule rather
// than parsing every list twice before building the blocking engine.
let has_rule = contents.lines().any(|line| {
@@ -479,24 +538,7 @@ pub(crate) async fn get_filter(
filter: &mut Filter,
http_client: &reqwest::Client,
) -> super::ConfigurationResult {
- let response = http_client.get(filter.url.as_str()).send().await?;
- if response.status().is_success() {
- validate_filter_content_type(filter, &response)?;
- let content = response.text().await?;
- validate_filter_rules(filter, &content)?;
- Ok(content)
- } else {
- log::error!(
- "Failed to fetch filter content for {}: {}",
- filter.title,
- response.status()
- );
- Err(super::ConfigurationError::FilterError(format!(
- "Failed to fetch filter content for {}: {}",
- filter.title,
- response.status()
- )))
- }
+ download::get_filter(filter, http_client).await
}
fn get_filter_directory() -> PathBuf {
@@ -512,7 +554,7 @@ pub(crate) async fn get_filters_content(
configuration: &mut super::Configuration,
http_client: &reqwest::Client,
filter_failure_store: &super::FilterFailureStore,
-) -> Vec {
+) -> Vec {
let mut filters = Vec::new();
let enabled_count = configuration
.filters
@@ -538,7 +580,11 @@ pub(crate) async fn get_filters_content(
filter.title,
filter_content.len()
);
- filters.push(filter_content);
+ filters.push(FilterList {
+ name: filter.title.clone(),
+ contents: filter_content,
+ trusted: is_trusted_source(&filter.url),
+ });
}
// A fetch failure here includes a filter whose URL has stopped serving a
// `text/plain` list (see `validate_filter_content_type`); we record it for the
@@ -561,10 +607,15 @@ pub(crate) async fn get_filters_content(
log::error!("No enabled filter lists could be loaded; only custom rules are available");
}
- filters.extend(configuration.custom_filters.iter().cloned());
- filters.sort_unstable();
- // Filter out duplicate lines, if present
- filters.dedup();
+ // Custom rules are explicitly supplied by the operator. Keep their order
+ // and scope together so conditional blocks can span multiple lines.
+ if !configuration.custom_filters.is_empty() {
+ filters.push(FilterList {
+ name: "Custom filters".to_owned(),
+ contents: configuration.custom_filters.join("\n"),
+ trusted: true,
+ });
+ }
filters
}
@@ -572,10 +623,91 @@ pub(crate) async fn get_filters_content(
mod tests {
use super::*;
- struct TestCache(PathBuf);
+ pub(super) fn test_client() -> reqwest::Client {
+ let _ = rustls::crypto::ring::default_provider().install_default();
+ reqwest::Client::builder().no_proxy().build().unwrap()
+ }
+
+ pub(super) fn subscription(url: &str, enabled: bool) -> Filter {
+ Filter {
+ enabled,
+ title: "Existing subscription".to_owned(),
+ group: FilterGroup::Default,
+ file_name: calc_filter_filename(url),
+ url: Url::parse(url).unwrap(),
+ }
+ }
+
+ #[test]
+ fn source_migration_preserves_settings_and_uses_new_cache_names() {
+ let mut filters: Vec<_> = ["filters", "badware", "privacy", "quick-fixes", "unbreak"]
+ .iter().enumerate().map(|(index, name)| subscription(
+ &format!("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/{name}.txt"),
+ index % 2 == 0,
+ )).collect();
+ let unchanged = subscription("https://example.test/custom.txt", false);
+ let mobile = subscription("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-mobile.txt", false);
+ filters.extend([unchanged.clone(), mobile.clone()]);
+ let old = filters.clone();
+ assert!(migrate_ublock_sources(&mut filters));
+ assert_eq!(filters.len(), old.len());
+ for (before, after) in old[..5].iter().zip(&filters[..5]) {
+ assert_eq!(before.enabled, after.enabled);
+ assert_eq!(before.group, after.group);
+ assert_ne!(before.file_name, after.file_name);
+ assert_eq!(after.file_name, calc_filter_filename(after.url.as_str()));
+ assert!(DefaultFilters::new()
+ .file_names()
+ .contains(&after.file_name));
+ }
+ assert_eq!(filters[5..], [unchanged, mobile]);
+ let migrated = filters.clone();
+ assert!(!migrate_ublock_sources(&mut filters));
+ assert_eq!(filters, migrated);
+ }
+
+ #[test]
+ fn existing_cdn_subscription_wins_over_legacy_duplicates() {
+ let cdn = "https://raw.githubusercontent.com/uBlockOrigin/uAssetsCDN/main/filters/filters.min.txt";
+ for enabled in [true, false] {
+ let mut canonical = subscription(cdn, enabled);
+ canonical.title = "My CDN subscription".to_owned();
+ let mut filters = vec![
+ subscription("https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt", !enabled),
+ canonical.clone(),
+ subscription("https://raw.githubusercontent.com/uBlockOrigin/uAssets/refs/heads/master/filters/filters.txt", !enabled),
+ ];
+ assert!(migrate_ublock_sources(&mut filters));
+ assert_eq!(filters, [canonical]);
+ }
+ }
+
+ #[test]
+ fn migration_recognizes_explicit_branch_refs_without_adding_absent_lists() {
+ let mut filters = vec![subscription("https://raw.githubusercontent.com/uBlockOrigin/uAssets/refs/heads/master/filters/privacy.txt", false)];
+ assert!(migrate_ublock_sources(&mut filters));
+ assert_eq!(filters.len(), 1);
+ assert!(!filters[0].enabled);
+ assert!(filters[0]
+ .url
+ .as_str()
+ .ends_with("uAssetsCDN/main/filters/privacy.min.txt"));
+ let mut empty = Vec::new();
+ assert!(!migrate_ublock_sources(&mut empty));
+ assert!(empty.is_empty());
+ }
+
+ #[test]
+ fn malformed_conditional_lists_are_not_accepted_into_the_cache() {
+ let filter = cached_filter();
+ assert!(validate_filter_rules(&filter, "!#if false\n||ads.example.com^").is_err());
+ assert!(validate_filter_rules(&filter, "!#if false\n||ads.example.com^\n!#endif").is_ok());
+ }
+
+ pub(super) struct TestCache(pub(super) PathBuf);
impl TestCache {
- fn new() -> Self {
+ pub(super) fn new() -> Self {
let directory = std::env::temp_dir().join(format!(
"privaxy-filter-cache-{}",
super::super::generate_random_hex(8)
@@ -612,7 +744,7 @@ mod tests {
let alternate = path.with_extension("");
let rules = "||ads.example.com^\nexample.com##.banner\n";
fs::write(&alternate, rules).await.unwrap();
- let client = reqwest::Client::new();
+ let client = test_client();
for invalid in [
None,
@@ -646,7 +778,7 @@ mod tests {
.unwrap();
assert_eq!(
filter
- .get_contents_in_directory(&reqwest::Client::new(), &cache.0)
+ .get_contents_in_directory(&test_client(), &cache.0)
.await
.unwrap(),
rules
@@ -663,7 +795,7 @@ mod tests {
.await
.unwrap();
let error = filter
- .get_contents_in_directory(&reqwest::Client::new(), &cache.0)
+ .get_contents_in_directory(&test_client(), &cache.0)
.await
.unwrap_err();
assert!(matches!(
diff --git a/privaxy/src/server/configuration/filter/download.rs b/privaxy/src/server/configuration/filter/download.rs
new file mode 100644
index 00000000..a85b6202
--- /dev/null
+++ b/privaxy/src/server/configuration/filter/download.rs
@@ -0,0 +1,401 @@
+//! Expand uBO includes before a subscription replaces its on-disk cache.
+use std::time::Duration;
+
+use url::Url;
+
+use super::super::{ConfigurationError, ConfigurationResult};
+use super::{validate_filter_content_type, validate_filter_rules, Filter};
+use crate::filter_list::{is_trusted_source, preprocess_for_download};
+
+const MAX_FILES: usize = 64;
+const MAX_DEPTH: usize = 16;
+const MAX_BYTES: usize = 64 * 1024 * 1024;
+
+/// uBO includes must stay in the parent's directory or one of its descendants.
+/// Reject URL/encoding tricks before joining, including encoded path traversal.
+fn include_url(parent: &Url, path: &str) -> Result {
+ if path.is_empty()
+ || path.starts_with('/')
+ || path.contains("..")
+ || path.contains(['\\', ':', '%', '?', '#'])
+ || path.chars().any(char::is_whitespace)
+ {
+ return Err("!#include must name a relative file in the same directory or a subdirectory");
+ }
+ let child = parent.join(path).map_err(|_| "invalid !#include path")?;
+ if !within_directory(parent, &child) {
+ return Err("!#include cannot leave its parent's directory or origin");
+ }
+ Ok(child)
+}
+
+fn within_directory(parent: &Url, child: &Url) -> bool {
+ let directory_end = parent.path().rfind('/').map_or(0, |index| index + 1);
+ matches!(parent.scheme(), "http" | "https")
+ && parent.origin() == child.origin()
+ && child.path().starts_with(&parent.path()[..directory_end])
+}
+
+struct Download<'a> {
+ filter: &'a Filter,
+ client: &'a reqwest::Client,
+ ancestors: Vec,
+ files: usize,
+ bytes: usize,
+}
+
+impl Download<'_> {
+ fn invalid(&self, message: impl std::fmt::Display) -> ConfigurationError {
+ ConfigurationError::FilterValidationError(format!("{}: {message}", self.filter.title))
+ }
+
+ async fn load(&mut self, url: &Url) -> ConfigurationResult {
+ if self.ancestors.contains(url) {
+ return Err(self.invalid("cyclic !#include"));
+ }
+ if self.ancestors.len() > MAX_DEPTH {
+ return Err(self.invalid("!#include nesting exceeds 16 levels"));
+ }
+ self.files += 1;
+ if self.files > MAX_FILES {
+ return Err(self.invalid("subscription exceeds 64 files"));
+ }
+ if is_trusted_source(&self.filter.url) && !is_trusted_source(url) {
+ return Err(self.invalid("!#include cannot leave the trusted filter source"));
+ }
+
+ let mut response = self
+ .client
+ .get(url.clone())
+ .timeout(Duration::from_secs(60))
+ .send()
+ .await?;
+ if !response.status().is_success() {
+ return Err(ConfigurationError::FilterError(format!(
+ "Failed to fetch filter content for {}: {}",
+ self.filter.title,
+ response.status()
+ )));
+ }
+ // The shared client disables redirects. Also check the final URL when
+ // called with another client, so fetched content cannot gain trust by
+ // redirecting out of the permitted source.
+ if (is_trusted_source(&self.filter.url) && !is_trusted_source(response.url()))
+ || self
+ .ancestors
+ .last()
+ .is_some_and(|parent| !within_directory(parent, response.url()))
+ {
+ return Err(self.invalid("filter redirect leaves the permitted source"));
+ }
+ validate_filter_content_type(self.filter, &response)?;
+ let headers = response.headers().clone();
+ let mut bytes = Vec::new();
+ while let Some(chunk) = response.chunk().await? {
+ self.bytes = self.bytes.saturating_add(chunk.len());
+ if self.bytes > MAX_BYTES {
+ return Err(self.invalid("combined subscription exceeds 64 MiB"));
+ }
+ bytes.extend_from_slice(&chunk);
+ }
+ // Preserve reqwest's charset decoding after reading a bounded body.
+ let mut response = reqwest::Response::from(http::Response::new(bytes));
+ *response.headers_mut() = headers;
+ let content = response.text().await?;
+ let parsed = preprocess_for_download(&content).map_err(|error| self.invalid(error))?;
+ if parsed.includes.is_empty() {
+ return Ok(content);
+ }
+
+ self.ancestors.push(url.clone());
+ let mut expanded = String::new();
+ let mut includes = parsed.includes.into_iter().peekable();
+ for (index, line) in content.lines().enumerate() {
+ if let Some(&(include_index, path)) = includes.peek() {
+ if index == include_index {
+ includes.next();
+ let child = include_url(url, path).map_err(|error| self.invalid(error))?;
+ let fragment = Box::pin(self.load(&child))
+ .await
+ .map_err(|error| self.invalid(format!("!#include {path}: {error}")))?;
+ self.append(&mut expanded, &format!("! Included: {path}\n"))?;
+ self.append(&mut expanded, &fragment)?;
+ self.append(&mut expanded, "\n! End include\n")?;
+ continue;
+ }
+ }
+ self.append(&mut expanded, line)?;
+ self.append(&mut expanded, "\n")?;
+ }
+ self.ancestors.pop();
+ Ok(expanded)
+ }
+
+ fn append(&self, output: &mut String, text: &str) -> ConfigurationResult<()> {
+ if output.len().saturating_add(text.len()) > MAX_BYTES {
+ return Err(self.invalid("expanded subscription exceeds 64 MiB"));
+ }
+ output.push_str(text);
+ Ok(())
+ }
+}
+
+pub(super) async fn get_filter(
+ filter: &Filter,
+ client: &reqwest::Client,
+) -> ConfigurationResult {
+ let mut download = Download {
+ filter,
+ client,
+ ancestors: Vec::new(),
+ files: 0,
+ bytes: 0,
+ };
+ let content = download.load(&filter.url).await?;
+ validate_filter_rules(filter, &content)?;
+ Ok(content)
+}
+
+#[cfg(test)]
+mod tests {
+ use std::collections::HashMap;
+ use std::sync::{Arc, Mutex};
+
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
+ use tokio::net::TcpListener;
+ use tokio::task::JoinHandle;
+
+ use super::*;
+ use crate::configuration::filter::tests::{subscription, test_client, TestCache};
+ use crate::filter_list::preprocess;
+
+ struct Server {
+ url: Url,
+ requests: Arc>>,
+ task: JoinHandle<()>,
+ }
+
+ impl Server {
+ async fn new(routes: &[(&str, &str)]) -> Self {
+ let routes: HashMap<_, _> = routes
+ .iter()
+ .map(|(path, body)| (path.to_string(), body.to_string()))
+ .collect();
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let url = Url::parse(&format!("http://{}/", listener.local_addr().unwrap())).unwrap();
+ let requests = Arc::new(Mutex::new(Vec::new()));
+ let recorded = Arc::clone(&requests);
+ let task = tokio::spawn(async move {
+ loop {
+ let (mut stream, _) = listener.accept().await.unwrap();
+ let mut request = Vec::new();
+ let mut chunk = [0; 1024];
+ while !request.ends_with(b"\r\n\r\n") {
+ let len = stream.read(&mut chunk).await.unwrap();
+ if len == 0 {
+ break;
+ }
+ request.extend_from_slice(&chunk[..len]);
+ }
+ let request = String::from_utf8(request).unwrap();
+ let path = request.split_whitespace().nth(1).unwrap();
+ recorded.lock().unwrap().push(path.to_owned());
+ let (status, body) = match routes.get(path) {
+ Some(body) => ("200 OK", body.as_str()),
+ None => ("404 Not Found", "missing"),
+ };
+ let mime = if body.starts_with("") {
+ "text/html"
+ } else {
+ "text/plain; charset=utf-8"
+ };
+ let response = format!(
+ "HTTP/1.1 {status}\r\nContent-Type: {mime}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
+ body.len()
+ );
+ stream.write_all(response.as_bytes()).await.unwrap();
+ }
+ });
+ Self {
+ url,
+ requests,
+ task,
+ }
+ }
+
+ fn filter(&self) -> Filter {
+ subscription(self.url.join("lists/main.txt").unwrap().as_str(), true)
+ }
+ }
+
+ impl Drop for Server {
+ fn drop(&mut self) {
+ self.task.abort();
+ }
+ }
+
+ #[test]
+ fn include_paths_cannot_leave_the_subscription_directory() {
+ let base = Url::parse("https://example.com/lists/main.txt").unwrap();
+ for path in ["child.txt", "nested/child.txt", "./child.txt"] {
+ assert!(include_url(&base, path).is_ok(), "{path}");
+ }
+ for path in [
+ "",
+ "../child.txt",
+ "/child.txt",
+ "//other.example/child.txt",
+ "https://other.example/child.txt",
+ "file:///etc/passwd",
+ "sub/../../child.txt",
+ "%2e%2e/child.txt",
+ "sub%2fchild.txt",
+ "sub\\child.txt",
+ "child.txt?url=other",
+ "child.txt#fragment",
+ ] {
+ assert!(include_url(&base, path).is_err(), "{path}");
+ }
+ assert!(!within_directory(
+ &base,
+ &Url::parse("https://example.com/lists-other/child.txt").unwrap()
+ ));
+ }
+
+ #[tokio::test]
+ async fn nested_includes_upgrade_old_caches_and_survive_offline_reload() {
+ let main = "!#if ext_ublock\n!#include child.txt\n!#else\n!#include missing.txt\n!#endif\n||after.example^";
+ let server = Server::new(&[
+ ("/lists/main.txt", main),
+ (
+ "/lists/child.txt",
+ "||child.example^\n!#include nested/leaf.txt",
+ ),
+ (
+ "/lists/nested/leaf.txt",
+ "!#if cap_html_filtering\n||unused.example^\n!#else\n||leaf.example^\n!#endif",
+ ),
+ ])
+ .await;
+ let cache = TestCache::new();
+ let mut filter = server.filter();
+ let path = cache.0.join(&filter.file_name);
+ // Old versions cached unresolved include directives as comments.
+ tokio::fs::write(&path, main).await.unwrap();
+ let client = test_client();
+ let expanded = filter
+ .get_contents_in_directory(&client, &cache.0)
+ .await
+ .unwrap();
+ let active = preprocess(&expanded).unwrap();
+ let rules: Vec<_> = active
+ .lines
+ .into_iter()
+ .filter(|line| line.starts_with("||"))
+ .collect();
+ assert_eq!(
+ rules,
+ ["||child.example^", "||leaf.example^", "||after.example^"]
+ );
+ assert_eq!(
+ *server.requests.lock().unwrap(),
+ [
+ "/lists/main.txt",
+ "/lists/child.txt",
+ "/lists/nested/leaf.txt"
+ ]
+ );
+ assert_eq!(tokio::fs::read_to_string(path).await.unwrap(), expanded);
+ drop(server);
+ assert_eq!(
+ filter
+ .get_contents_in_directory(&client, &cache.0)
+ .await
+ .unwrap(),
+ expanded
+ );
+ }
+
+ #[tokio::test]
+ async fn failed_includes_preserve_the_last_complete_cache() {
+ for (fragment, expected_error) in [
+ (None, "404"),
+ (Some("not a list"), "text/html"),
+ (Some("!#endif\n||bad.example^"), "unmatched !#endif"),
+ (Some("!#include main.txt"), "cyclic !#include"),
+ (Some("!#include ../outside.txt"), "relative file"),
+ ] {
+ let mut routes = vec![("/lists/main.txt", "||new.example^\n!#include child.txt")];
+ if let Some(fragment) = fragment {
+ routes.push(("/lists/child.txt", fragment));
+ }
+ let server = Server::new(&routes).await;
+ let mut filter = server.filter();
+ let client = test_client();
+ let cache = TestCache::new();
+ let path = cache.0.join(&filter.file_name);
+ let previous = "||previous.example^\n";
+ tokio::fs::write(&path, previous).await.unwrap();
+ let error = filter
+ .update_in_directory(&client, &cache.0)
+ .await
+ .unwrap_err()
+ .to_string();
+ assert!(error.contains(expected_error), "{error}");
+ assert_eq!(tokio::fs::read_to_string(path).await.unwrap(), previous);
+ assert_eq!(
+ filter
+ .get_contents_in_directory(&client, &cache.0)
+ .await
+ .unwrap(),
+ previous
+ );
+ }
+ }
+
+ #[tokio::test]
+ async fn subscriptions_can_consist_only_of_includes() {
+ let server = Server::new(&[
+ ("/lists/main.txt", "!#include child.txt"),
+ ("/lists/child.txt", "||ads.example^"),
+ ])
+ .await;
+ let result = get_filter(&server.filter(), &test_client()).await.unwrap();
+ assert!(result.contains("||ads.example^"));
+ }
+
+ #[tokio::test]
+ async fn download_limits_and_trust_are_enforced() {
+ let server = Server::new(&[("/lists/main.txt", "||ads.example^")]).await;
+ let filter = server.filter();
+ let client = test_client();
+ let mut download = Download {
+ filter: &filter,
+ client: &client,
+ ancestors: Vec::new(),
+ files: MAX_FILES,
+ bytes: 0,
+ };
+ let error = download.load(&filter.url).await.unwrap_err().to_string();
+ assert!(error.contains("64 files"));
+ download.files = 0;
+ download.ancestors = vec![server.url.clone(); MAX_DEPTH + 1];
+ let error = download.load(&filter.url).await.unwrap_err().to_string();
+ assert!(error.contains("nesting exceeds"));
+ download.ancestors.clear();
+ download.bytes = MAX_BYTES;
+ let error = download.load(&filter.url).await.unwrap_err().to_string();
+ assert!(error.contains("64 MiB"));
+ assert_eq!(server.requests.lock().unwrap().len(), 1);
+
+ let trusted = subscription(
+ "https://raw.githubusercontent.com/uBlockOrigin/uAssetsCDN/main/filters/filters.min.txt",
+ true,
+ );
+ download.filter = &trusted;
+ download.bytes = 0;
+ let error = download.load(&filter.url).await.unwrap_err().to_string();
+ assert!(error.contains("trusted filter source"));
+ assert_eq!(server.requests.lock().unwrap().len(), 1);
+ }
+}
diff --git a/privaxy/src/server/configuration/mod.rs b/privaxy/src/server/configuration/mod.rs
index 1cac41ae..412f4159 100644
--- a/privaxy/src/server/configuration/mod.rs
+++ b/privaxy/src/server/configuration/mod.rs
@@ -7,6 +7,7 @@ mod ca;
mod filter;
mod filter_failures;
mod network;
+mod statistics;
mod updater;
mod userscript;
pub use auth::*;
@@ -15,6 +16,7 @@ pub use filter::*;
pub use filter_failures::*;
use futures::future::join_all;
pub use network::*;
+pub use statistics::*;
use std::env;
use std::path::{Path, PathBuf};
pub use updater::*;
@@ -85,6 +87,8 @@ pub struct Configuration {
pub auth: Auth,
#[serde(default)]
pub debug: DebugConfig,
+ #[serde(default)]
+ pub statistics: StatisticsConfig,
/// Userscripts and the master switch governing them. Absent from
/// configuration files written before the feature existed, hence the serde
/// default.
@@ -144,7 +148,8 @@ impl Configuration {
Ok(bytes) => {
let mut configuration: Configuration =
toml::from_str(std::str::from_utf8(&bytes)?)?;
- if configuration.ensure_auth_keys() {
+ let migrated_filters = migrate_ublock_sources(&mut configuration.filters);
+ if configuration.ensure_auth_keys() || migrated_filters {
configuration.save().await?;
}
Ok(configuration)
@@ -588,6 +593,7 @@ impl Configuration {
custom_filters: Vec::new(),
auth: Auth::new_initialized(),
debug: DebugConfig::default(),
+ statistics: StatisticsConfig::default(),
userscripts: UserScriptsConfig::default(),
})
}
diff --git a/privaxy/src/server/configuration/statistics.rs b/privaxy/src/server/configuration/statistics.rs
new file mode 100644
index 00000000..cee23ecb
--- /dev/null
+++ b/privaxy/src/server/configuration/statistics.rs
@@ -0,0 +1,82 @@
+use serde::{Deserialize, Serialize};
+use std::{collections::BTreeMap, net::IpAddr};
+
+/// These levels describe collected data, not just what the UI displays.
+#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
+#[serde(rename_all = "snake_case")]
+pub enum StatisticsPrivacy {
+ Full,
+ HideDomains,
+ #[default]
+ HideDomainsAndClients,
+ Anonymous,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+#[serde(default, deny_unknown_fields)]
+pub struct StatisticsConfig {
+ pub enabled: bool,
+ pub privacy: StatisticsPrivacy,
+ pub retention_days: u16,
+ pub max_entries: u32,
+ pub excluded_domains: Vec,
+ pub excluded_clients: Vec,
+ pub client_names: BTreeMap,
+}
+
+impl Default for StatisticsConfig {
+ fn default() -> Self {
+ Self {
+ enabled: false,
+ privacy: StatisticsPrivacy::default(),
+ retention_days: 7,
+ max_entries: 100_000,
+ excluded_domains: Vec::new(),
+ excluded_clients: Vec::new(),
+ client_names: BTreeMap::new(),
+ }
+ }
+}
+
+impl StatisticsConfig {
+ pub fn validate(&self) -> Result<(), String> {
+ if self.excluded_clients.len() > 1000 {
+ return Err("Use at most 1,000 client IP exclusions".into());
+ }
+ if self.client_names.len() > 1000
+ || self.client_names.values().any(|name| {
+ name.trim().is_empty()
+ || name.chars().count() > 64
+ || name.chars().any(char::is_control)
+ })
+ {
+ return Err(
+ "Use at most 1,000 client names, each 1–64 characters without control characters"
+ .into(),
+ );
+ }
+ if self.excluded_domains.len() > 1000
+ || self.excluded_domains.iter().any(|pattern| {
+ let pattern = pattern.trim();
+ pattern.is_empty()
+ || pattern.len() > 253
+ || !pattern
+ .bytes()
+ .all(|b| b.is_ascii_alphanumeric() || b".-*?".contains(&b))
+ })
+ {
+ return Err("Use at most 1,000 domain patterns, without schemes, ports, or paths (for example example.com or *.example.com)".into());
+ }
+ if !(1..=365).contains(&self.retention_days) {
+ return Err("Statistics retention must be between 1 and 365 days".into());
+ }
+ if !(100..=1_000_000).contains(&self.max_entries) {
+ return Err("Request history limit must be between 100 and 1,000,000 entries".into());
+ }
+ Ok(())
+ }
+
+ pub fn persists(&self) -> bool {
+ self.enabled && self.privacy != StatisticsPrivacy::Anonymous
+ }
+}
diff --git a/privaxy/src/server/filter_list.rs b/privaxy/src/server/filter_list.rs
new file mode 100644
index 00000000..2b9392c2
--- /dev/null
+++ b/privaxy/src/server/filter_list.rs
@@ -0,0 +1,284 @@
+//! uBO conditional directives and source permissions, before adblock-rust parsing.
+use adblock::resources::PermissionMask;
+use url::Url;
+
+/// Also used by build-scriptlets.mjs for upstream `requiresTrust` resources.
+pub(crate) const TRUSTED_SCRIPTLETS: PermissionMask = PermissionMask::from_bits(1);
+
+pub(crate) struct FilterList {
+ pub name: String,
+ pub contents: String,
+ pub trusted: bool,
+}
+
+/// Only the official uBO filter repositories have automatic scriptlet trust.
+/// In particular, neither a lookalike host nor arbitrary GitHub lists qualify.
+pub(crate) fn is_trusted_source(url: &Url) -> bool {
+ url.scheme() == "https"
+ && url.host_str() == Some("raw.githubusercontent.com")
+ && url.port().is_none()
+ && url.username().is_empty()
+ && url.password().is_none()
+ && [
+ "/uBlockOrigin/uAssets/master/filters/",
+ "/uBlockOrigin/uAssets/refs/heads/master/filters/",
+ "/uBlockOrigin/uAssetsCDN/main/filters/",
+ ]
+ .iter()
+ .any(|prefix| {
+ url.path().strip_prefix(prefix).is_some_and(|name| {
+ name.ends_with(".txt")
+ && name.bytes().all(|byte| {
+ byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')
+ })
+ })
+ })
+}
+
+/// A portable uBO syntax profile. A shared proxy cannot assume its clients use
+/// Firefox, Chromium, or a mobile browser. HTML rewriting for injection does not
+/// implement uBO HTML filtering/$replace or $ipaddress, so those capabilities
+/// must remain false and lists can select their scriptlet fallbacks instead.
+fn token_value(token: &str) -> Option {
+ let (negated, token) = match token.strip_prefix('!') {
+ Some(token) => (true, token),
+ None => (false, token),
+ };
+ let value = match token {
+ "ext_ublock" => true,
+ "false"
+ | "ext_ubol"
+ | "ext_devbuild"
+ | "ext_abp"
+ | "adguard"
+ | "env_brave"
+ | "env_chromium"
+ | "env_edge"
+ | "env_firefox"
+ | "env_legacy"
+ | "env_mobile"
+ | "env_mv3"
+ | "env_safari"
+ | "adguard_app_android"
+ | "adguard_app_cli"
+ | "adguard_app_ios"
+ | "adguard_app_mac"
+ | "adguard_app_windows"
+ | "adguard_ext_android_cb"
+ | "adguard_ext_chromium"
+ | "adguard_ext_chromium_mv3"
+ | "adguard_ext_edge"
+ | "adguard_ext_firefox"
+ | "adguard_ext_opera"
+ | "adguard_ext_safari" => false,
+ token if token.starts_with("cap_") => false,
+ _ => return None,
+ };
+ Some(value != negated)
+}
+
+// uBO evaluates a chain from left to right, with optional outer parentheses.
+// Unrecognized expressions remain unknown, as in uBO: neither branch is removed.
+fn evaluate_condition(expression: &str) -> Option {
+ let expression = expression.trim();
+ let expression = expression
+ .strip_prefix('(')
+ .and_then(|inner| inner.strip_suffix(')'))
+ .unwrap_or(expression);
+ let mut tokens = expression.split_whitespace();
+ let mut result = token_value(tokens.next()?)?;
+ while let Some(operator) = tokens.next() {
+ let rhs = token_value(tokens.next()?)?;
+ result = match operator {
+ "&&" => result && rhs,
+ "||" => result || rhs,
+ _ => return None,
+ };
+ }
+ Some(result)
+}
+
+struct Conditional {
+ parent_active: bool,
+ condition: Option,
+ in_else: bool,
+}
+
+pub(crate) struct Preprocessed<'a> {
+ pub lines: Vec<&'a str>,
+ pub unknown_conditions: usize,
+ /// Zero-based source line and relative path, in source order.
+ pub includes: Vec<(usize, &'a str)>,
+}
+
+/// Keep directive scope local to each list. Reject malformed structure instead
+/// of letting an unmatched directive accidentally disable the rest of a list.
+pub(crate) fn preprocess(contents: &str) -> Result, String> {
+ preprocess_inner(contents, false)
+}
+
+/// Find active includes without losing the original conditional blocks. Each
+/// downloaded fragment is checked separately so it cannot close a parent's if.
+pub(crate) fn preprocess_for_download(contents: &str) -> Result, String> {
+ preprocess_inner(contents, true)
+}
+
+fn preprocess_inner(contents: &str, allow_includes: bool) -> Result, String> {
+ let mut result = Preprocessed {
+ lines: Vec::new(),
+ unknown_conditions: 0,
+ includes: Vec::new(),
+ };
+ let mut stack: Vec = Vec::new();
+ let mut active = true;
+ for (index, line) in contents.lines().enumerate() {
+ let line = line.trim().trim_start_matches('\u{feff}');
+ let (directive, argument) = line.split_once(char::is_whitespace).unwrap_or((line, ""));
+ let error = |message| format!("line {}: {message}", index + 1);
+ match directive {
+ "!#if" => {
+ if argument.trim().is_empty() {
+ return Err(error("missing condition"));
+ }
+ if stack.len() >= 128 {
+ return Err(error("conditional nesting exceeds 128 levels"));
+ }
+ let condition = evaluate_condition(argument);
+ if condition.is_none() {
+ result.unknown_conditions += 1;
+ }
+ stack.push(Conditional {
+ parent_active: active,
+ condition,
+ in_else: false,
+ });
+ active &= condition.unwrap_or(true);
+ }
+ "!#else" => {
+ let frame = stack.last_mut().ok_or_else(|| error("unmatched !#else"))?;
+ if frame.in_else {
+ return Err(error("duplicate !#else"));
+ }
+ frame.in_else = true;
+ active = frame.parent_active && frame.condition.map(|value| !value).unwrap_or(true);
+ }
+ "!#endif" => {
+ let frame = stack.pop().ok_or_else(|| error("unmatched !#endif"))?;
+ active = frame.parent_active;
+ }
+ "!#include" if active => {
+ if !allow_includes {
+ return Err(error(
+ "unresolved !#include; refresh the subscription to download its sub-lists",
+ ));
+ }
+ let path = argument
+ .split_whitespace()
+ .next()
+ .ok_or_else(|| error("missing !#include path"))?;
+ result.includes.push((index, path));
+ }
+ _ if active => result.lines.push(line),
+ _ => {}
+ }
+ }
+ if !stack.is_empty() {
+ return Err("unterminated !#if".to_owned());
+ }
+ Ok(result)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn nested_conditions_select_portable_scriptlet_fallbacks() {
+ let input = "!#if cap_html_filtering\nreplace-rule\n!#else\nscriptlet-rule\n!#if env_firefox\nfirefox-only\n!#else\nportable\n!#endif\n!#endif\nafter";
+ assert_eq!(
+ preprocess(input).unwrap().lines,
+ ["scriptlet-rule", "portable", "after"]
+ );
+ let input =
+ "!#if false\n!#if !false\nhidden\n!#else\nalso-hidden\n!#endif\n!#endif\nvisible";
+ assert_eq!(preprocess(input).unwrap().lines, ["visible"]);
+ }
+
+ #[test]
+ fn conditions_follow_ubo_left_to_right_boolean_chains() {
+ assert_eq!(
+ evaluate_condition("(!ext_ubol && !cap_html_filtering)"),
+ Some(true)
+ );
+ assert_eq!(evaluate_condition("!false || false && false"), Some(false));
+ assert_eq!(evaluate_condition("cap_future_feature"), Some(false));
+ assert_eq!(evaluate_condition("adguard_future_platform"), None);
+ assert_eq!(evaluate_condition("!cap_future_feature"), Some(true));
+ assert_eq!(
+ evaluate_condition("env_firefox || env_chromium"),
+ Some(false)
+ );
+ }
+
+ #[test]
+ fn unknown_conditions_preserve_both_branches_like_ubo() {
+ let parsed = preprocess("!#if future_token\na\n!#else\nb\n!#endif").unwrap();
+ assert_eq!(parsed.lines, ["a", "b"]);
+ assert_eq!(parsed.unknown_conditions, 1);
+ }
+
+ #[test]
+ fn only_active_includes_are_downloaded_and_keep_source_positions() {
+ let input = "!#if false\n!#include ignored.txt\n!#else\n!#include first.txt comment\n!#endif\n!#if future_token\n!#include second.txt\n!#else\n!#include third.txt\n!#endif";
+ let parsed = preprocess_for_download(input).unwrap();
+ assert_eq!(
+ parsed.includes,
+ [(3, "first.txt"), (6, "second.txt"), (8, "third.txt")]
+ );
+ assert_eq!(parsed.unknown_conditions, 1);
+ assert!(preprocess_for_download("!#include").is_err());
+ assert!(preprocess_for_download("!#if false\n!#include\n!#endif").is_ok());
+ }
+
+ #[test]
+ fn malformed_directives_and_active_includes_fail() {
+ for input in [
+ "!#else",
+ "!#endif",
+ "!#if",
+ "!#if false",
+ "!#if false\n!#else\n!#else\n!#endif",
+ "!#include more.txt",
+ ] {
+ assert!(preprocess(input).is_err(), "{input}");
+ }
+ assert!(preprocess("!#if false\n!#include unused.txt\n!#endif").is_ok());
+ assert!(preprocess(&"!#if !false\n".repeat(129)).is_err());
+ assert_eq!(
+ preprocess("\u{feff}!#if !false\r\na\r\n!#endif\r\n")
+ .unwrap()
+ .lines,
+ ["a"]
+ );
+ }
+
+ #[test]
+ fn scriptlet_trust_is_limited_to_official_sources() {
+ for url in [
+ "https://raw.githubusercontent.com/uBlockOrigin/uAssetsCDN/main/filters/filters.min.txt",
+ "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters-mobile.txt",
+ ] {
+ assert!(is_trusted_source(&Url::parse(url).unwrap()));
+ }
+ for url in [
+ "http://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/filters.txt",
+ "https://raw.githubusercontent.com.evil.test/uBlockOrigin/uAssets/master/filters/filters.txt",
+ "https://raw.githubusercontent.com/another/repo/main/filters/filters.txt",
+ "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/../other.txt",
+ "https://raw.githubusercontent.com/uBlockOrigin/uAssets/master/filters/..%2F..%2Fother.txt",
+ "https://raw.githubusercontent.com:8443/uBlockOrigin/uAssets/master/filters/filters.txt",
+ ] {
+ assert!(!is_trusted_source(&Url::parse(url).unwrap()));
+ }
+ }
+}
diff --git a/privaxy/src/server/lib.rs b/privaxy/src/server/lib.rs
index 7e329554..fb614cd7 100644
--- a/privaxy/src/server/lib.rs
+++ b/privaxy/src/server/lib.rs
@@ -3,17 +3,15 @@ use crate::configuration::{FilterFailureStore, NetworkConfig};
use crate::proxy::exclusions::LocalExclusionStore;
use crate::web_gui::events::Event;
use hyper::service::service_fn;
-use hyper_util::client::legacy::Client;
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto;
use hyper_util::server::graceful::GracefulShutdown;
use include_dir::{include_dir, Dir};
use proxy::exclusions;
use proxy::gm::storage::GmStorageStore;
-use proxy::serve::UpgradeClient;
use proxy::tls_failures::TlsFailureStore;
+use proxy::upstream::{http_client_builder, UpstreamClients};
use proxy::userscripts::{PrivateNetworkAccess, UserScriptContext, UserScriptStore};
-use reqwest::redirect::Policy;
use std::env;
use std::net::IpAddr;
use std::net::SocketAddr;
@@ -30,6 +28,7 @@ mod blocker_utils;
mod ca;
mod cert;
pub mod configuration;
+mod filter_list;
pub mod logging;
mod proxy;
pub mod statistics;
@@ -104,7 +103,7 @@ pub(crate) fn parse_ip_address(ip_str: &str) -> IpAddr {
IpAddr::from_str(ip_str).unwrap()
}
-async fn handle_signals() -> (Arc, Arc) {
+async fn handle_signals(statistics: statistics::Statistics) -> (Arc, Arc) {
let notify_shutdown = Arc::new(Notify::new());
let notify_reload = Arc::new(Notify::new());
let notify_shutdown_clone = notify_shutdown.clone();
@@ -115,6 +114,8 @@ async fn handle_signals() -> (Arc, Arc) {
signal(SignalKind::hangup()).expect("failed to set up SIGHUP signal handler");
let mut term_signal =
signal(SignalKind::terminate()).expect("failed to set up SIGTERM signal handler");
+ let mut int_signal =
+ signal(SignalKind::interrupt()).expect("failed to set up SIGINT signal handler");
loop {
tokio::select! {
@@ -122,9 +123,16 @@ async fn handle_signals() -> (Arc, Arc) {
log::info!("Received SIGHUP signal, restarting child processes...");
notify_reload_clone.notify_waiters();
}
- _ = term_signal.recv() => {
- log::info!("Received SIGTERM signal, shutting down gracefully...");
+ _ = async { tokio::select! { _ = term_signal.recv() => {}, _ = int_signal.recv() => {} } } => {
+ log::info!("Shutting down; flushing persistent statistics...");
notify_shutdown_clone.notify_waiters();
+ if let Some(history) = &statistics.history {
+ match tokio::time::timeout(Duration::from_secs(8), history.shutdown()).await {
+ Ok(Ok(())) => {},
+ Ok(Err(error)) => log::error!("Could not flush statistics: {error}"),
+ Err(_) => log::error!("Timed out flushing statistics"),
+ }
+ }
std::process::exit(0);
}
}
@@ -149,38 +157,7 @@ pub async fn start_privaxy() -> PrivaxyServer {
// We use reqwest instead of hyper's client to perform most of the proxying as it's more convenient
// to handle compression as well as offers a more convenient interface.
- let client = reqwest::Client::builder()
- .use_rustls_tls()
- .redirect(Policy::none())
- .no_proxy()
- .gzip(true)
- .brotli(true)
- .deflate(true)
- // Without these, a proxied request can hang indefinitely on a pooled
- // keep-alive connection the remote has silently dropped: reqwest reuses
- // the dead connection and waits on a peer that will never answer.
- // Retiring idle connections quickly (well under typical server keep-alive
- // windows) plus OS-level keepalive probes bounds that. `connect_timeout`
- // additionally fails fast on unreachable hosts.
- .connect_timeout(Duration::from_secs(10))
- .pool_idle_timeout(Duration::from_secs(30))
- // Bounds a stalled read mid-response: a peer that stops sending without
- // closing would otherwise hang the proxied request forever. Generous
- // enough (5 minutes between reads) not to kill long-polls or quiet SSE
- // streams, which routinely idle for a minute or two between events.
- .read_timeout(Duration::from_secs(300))
- // h2-heavy origins multiplex every subresource over a
- // single connection whose flow-control window defaults to a small,
- // shared 64 KB. When we drain one stream's body slowly (the browser
- // reads slowly, or the HTML rewriter backpressures), that window fills
- // and stalls *every other stream* on the connection — head-of-line
- // stutter across the whole site. Adaptive flow control grows the
- // stream/connection windows based on the bandwidth-delay product,
- // relieving the stall while keeping multiplexing. (This overrides any
- // manual http2_initial_*_window_size, which is why none are set.)
- .http2_adaptive_window(true)
- .build()
- .unwrap();
+ let client = http_client_builder().build().unwrap();
let configuration = match configuration::Configuration::read_from_home().await {
Ok(configuration) => configuration,
@@ -220,7 +197,7 @@ pub async fn start_privaxy() -> PrivaxyServer {
}
};
- let statistics = statistics::Statistics::new();
+ let statistics = statistics::Statistics::with_history(configuration.statistics.clone()).await;
let statistics_clone = statistics.clone();
let tls_failure_store = TlsFailureStore::new(configuration.ignored_tls_failures.clone());
@@ -272,7 +249,7 @@ pub async fn start_privaxy() -> PrivaxyServer {
let configuration_save_lock = Arc::new(tokio::sync::Mutex::new(()));
- let (_notify_shutdown, notify_reload) = handle_signals().await;
+ let (_notify_shutdown, notify_reload) = handle_signals(statistics.clone()).await;
let block_disable_ref = blocking_disabled_store.clone();
let local_exclusion_store_ref = local_exclusion_store.clone();
@@ -361,6 +338,19 @@ pub async fn start_privaxy() -> PrivaxyServer {
// The UI updates this store live; SIGHUP must also pick up file
// edits to the mode and both inclusion/exclusion lists.
local_exclusion_store.replace_configuration(&cfg);
+ {
+ // Serialize this re-read/apply with the settings API. A reload
+ // must not restore a stale privacy policy over a concurrent save.
+ let _guard = cfg_lock_backend.lock().await;
+ match configuration::Configuration::read_from_home().await {
+ Ok(current) => {
+ if let Err(error) = statistics.configure_history(current.statistics).await {
+ log::error!("Could not apply statistics settings: {error}");
+ }
+ }
+ Err(error) => log::error!("Could not reload statistics settings: {error}"),
+ }
+ }
// Same for the TLS-failure ignore set: pick up hand-edited
// `ignored_tls_failures` entries on SIGHUP reload.
tls_failure_store.set_ignored(cfg.ignored_tls_failures.clone());
@@ -742,21 +732,7 @@ async fn privaxy_backend(
configuration_save_lock: Arc>,
notify_reload: Arc,
) {
- // Mirror the reqwest client's connection hardening (see above): without a
- // connect timeout and OS-level keepalive, an upgrade can hang on a pooled
- // keep-alive connection the remote has silently dropped, surfacing as
- // "upgrade expected but not completed".
- let mut http_connector = hyper_util::client::legacy::connect::HttpConnector::new();
- http_connector.enforce_http(false);
- http_connector.set_connect_timeout(Some(Duration::from_secs(10)));
- http_connector.set_keepalive(Some(Duration::from_secs(60)));
-
- let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
- .with_native_roots()
- .expect("failed to load native root certificates")
- .https_or_http()
- .enable_http1()
- .wrap_connector(http_connector);
+ let clients = UpstreamClients::new(client).expect("failed to load native root certificates");
let config = match read_configuration(&configuration_save_lock).await {
Ok(config) => config,
Err(err) => {
@@ -785,17 +761,6 @@ async fn privaxy_backend(
allow_private_network_requests: private_network_access,
};
- // The hyper client is only used to perform upgrades. We don't need to
- // handle compression.
- // Hyper's client don't follow redirects, which is what we want, nothing to
- // disable here.
- // An upgraded connection is consumed by the tunnel anyway, so idle pooling
- // buys nothing and only risks reusing a stale connection under a long-lived
- // WebSocket — disable it.
- let hyper_client: UpgradeClient = Client::builder(TokioExecutor::new())
- .pool_max_idle_per_host(0)
- .build(https_connector);
-
let ip = env_or_config_ip(network_config).await;
let proxy_server_addr = SocketAddr::from((ip, network_config.proxy_port));
@@ -835,6 +800,9 @@ async fn privaxy_backend(
}
};
+ // Flush small TLS records and streamed response chunks promptly;
+ // Nagle plus delayed ACKs otherwise introduces avoidable stalls.
+ let _ = stream.set_nodelay(true);
// Reproduce the old `Server::tcp_keepalive(600s)`: enable
// OS-level keepalive on each accepted socket.
let _ = socket2::SockRef::from(&stream).set_tcp_keepalive(
@@ -842,8 +810,7 @@ async fn privaxy_backend(
);
let client_ip_address = peer_addr.ip();
- let client = client.clone();
- let hyper_client = hyper_client.clone();
+ let clients = clients.clone();
let cert_cache = cert_cache.clone();
let blocker_requester = blocker_requester.clone();
let broadcast_tx = broadcast_tx.clone();
@@ -861,8 +828,7 @@ async fn privaxy_backend(
let service = service_fn(move |req| {
proxy::serve_mitm_session(
blocker_requester.clone(),
- hyper_client.clone(),
- client.clone(),
+ clients.clone(),
req,
cert_cache.clone(),
broadcast_tx.clone(),
diff --git a/privaxy/src/server/proxy/client_hello.rs b/privaxy/src/server/proxy/client_hello.rs
new file mode 100644
index 00000000..d8fc582a
--- /dev/null
+++ b/privaxy/src/server/proxy/client_hello.rs
@@ -0,0 +1,47 @@
+//! Read the ClientHello before choosing an interception policy or certificate.
+use std::io;
+use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, BufReader};
+use tokio_rustls::StartHandshake;
+
+// Bound both the TLS parser's input and the copy retained for a possible tunnel.
+const MAX_CLIENT_HELLO_BYTES: usize = 64 * 1024;
+pub(super) const HANDSHAKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
+
+pub(super) async fn read_client_hello(
+ mut stream: BufReader,
+) -> io::Result<(StartHandshake>, Vec)>
+where
+ IO: AsyncRead + AsyncWrite + Unpin,
+{
+ let mut acceptor = rustls::server::Acceptor::default();
+ let mut prefix = Vec::new();
+ loop {
+ let remaining = MAX_CLIENT_HELLO_BYTES - prefix.len();
+ if remaining == 0 {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidData,
+ "ClientHello too large",
+ ));
+ }
+ let buffered = stream.fill_buf().await?;
+ let available = &buffered[..buffered.len().min(remaining)];
+ let consumed = acceptor.read_tls(&mut &available[..])?;
+ if consumed == 0 {
+ return Err(io::ErrorKind::UnexpectedEof.into());
+ }
+ prefix.extend_from_slice(&available[..consumed]);
+ stream.consume(consumed);
+ match acceptor.accept() {
+ Ok(Some(accepted)) => {
+ // rustls retains the parsed handshake for interception. For a
+ // bypass, replay exactly the consumed TLS bytes, then drain
+ // BufReader (including hyper's CONNECT read-ahead) normally.
+ return Ok((StartHandshake::from_parts(accepted, stream), prefix));
+ }
+ Ok(None) => {} // TCP and TLS-record fragmentation are both valid.
+ Err((error, _alert)) => {
+ return Err(io::Error::new(io::ErrorKind::InvalidData, error));
+ }
+ }
+ }
+}
diff --git a/privaxy/src/server/proxy/connect_tests.rs b/privaxy/src/server/proxy/connect_tests.rs
new file mode 100644
index 00000000..cda8fdf0
--- /dev/null
+++ b/privaxy/src/server/proxy/connect_tests.rs
@@ -0,0 +1,1164 @@
+//! Local CONNECT/TLS regressions. All keys and trusted roots are ephemeral;
+//! these fixtures never load configuration, production CAs or public services.
+use super::*;
+use crate::blocker::BlockingDisabledStore;
+use crate::proxy::{gm::storage::GmStorageStore, userscripts::UserScriptStore};
+use http_body_util::BodyExt;
+use openssl::{
+ asn1::Asn1Time,
+ bn::{BigNum, MsbOption},
+ hash::MessageDigest,
+ pkey::{PKey, Private},
+ rsa::Rsa,
+ x509::{
+ extension::{BasicConstraints, ExtendedKeyUsage, SubjectAlternativeName},
+ X509NameBuilder, X509,
+ },
+};
+use rustls::{
+ pki_types::{CertificateDer, PrivateKeyDer, ServerName},
+ ClientConfig, ServerConfig,
+};
+use std::{
+ collections::BTreeMap,
+ io,
+ sync::{OnceLock, RwLock},
+ time::Duration,
+};
+use tokio::{
+ net::TcpListener,
+ sync::mpsc,
+ task::{JoinHandle, JoinSet},
+ time::timeout,
+};
+use tokio_rustls::{client::TlsStream, LazyConfigAcceptor, TlsConnector};
+
+const TEST_TIMEOUT: Duration = Duration::from_secs(15);
+
+struct Pki {
+ proxy: (X509, PKey),
+ upstream: (X509, PKey),
+ untrusted: (X509, PKey),
+ leaf_key: PKey,
+}
+
+fn pki() -> &'static Pki {
+ static PKI: OnceLock = OnceLock::new();
+ PKI.get_or_init(|| {
+ let _ = rustls::crypto::ring::default_provider().install_default();
+ Pki {
+ proxy: crate::ca::make_ca_certificate(),
+ upstream: crate::ca::make_ca_certificate(),
+ untrusted: crate::ca::make_ca_certificate(),
+ leaf_key: PKey::from_rsa(Rsa::generate(2048).unwrap()).unwrap(),
+ }
+ })
+}
+
+fn der(cert: &X509) -> CertificateDer<'static> {
+ cert.to_der().unwrap().into()
+}
+
+// Independent origin certificates: don't use Privaxy's leaf generator for
+// both sides of a hostname-verification regression.
+fn origin_config(host: &str, trusted: bool, expired: bool) -> Arc {
+ let pki = pki();
+ let (ca, key) = if trusted {
+ &pki.upstream
+ } else {
+ &pki.untrusted
+ };
+ let mut cert = X509::builder().unwrap();
+ cert.set_version(2).unwrap();
+ let mut serial = BigNum::new().unwrap();
+ serial.rand(128, MsbOption::MAYBE_ZERO, false).unwrap();
+ cert.set_serial_number(&serial.to_asn1_integer().unwrap())
+ .unwrap();
+ let mut subject = X509NameBuilder::new().unwrap();
+ subject.append_entry_by_text("CN", host).unwrap();
+ cert.set_subject_name(&subject.build()).unwrap();
+ cert.set_issuer_name(ca.subject_name()).unwrap();
+ cert.set_pubkey(&pki.leaf_key).unwrap();
+ let (start, end) = if expired {
+ (
+ Asn1Time::from_unix(1).unwrap(),
+ Asn1Time::from_unix(2).unwrap(),
+ )
+ } else {
+ (
+ Asn1Time::days_from_now(0).unwrap(),
+ Asn1Time::days_from_now(1).unwrap(),
+ )
+ };
+ cert.set_not_before(&start).unwrap();
+ cert.set_not_after(&end).unwrap();
+ cert.append_extension(BasicConstraints::new().build().unwrap())
+ .unwrap();
+ cert.append_extension(ExtendedKeyUsage::new().server_auth().build().unwrap())
+ .unwrap();
+ let mut san = SubjectAlternativeName::new();
+ if host.parse::().is_ok() {
+ san.ip(host);
+ } else {
+ san.dns(host);
+ }
+ cert.append_extension(san.build(&cert.x509v3_context(Some(ca), None)).unwrap())
+ .unwrap();
+ cert.sign(key, MessageDigest::sha256()).unwrap();
+ let config = ServerConfig::builder()
+ .with_no_client_auth()
+ .with_single_cert(
+ vec![der(&cert.build()), der(ca)],
+ PrivateKeyDer::try_from(pki.leaf_key.private_key_to_der().unwrap()).unwrap(),
+ )
+ .unwrap();
+ Arc::new(config)
+}
+
+fn client_config(proxy: bool, sni: bool) -> Arc {
+ let mut roots = rustls::RootCertStore::empty();
+ roots
+ .add(der(if proxy {
+ &pki().proxy.0
+ } else {
+ &pki().upstream.0
+ }))
+ .unwrap();
+ let mut config = ClientConfig::builder()
+ .with_root_certificates(roots)
+ .with_no_client_auth();
+ // Disabling SNI does not disable certificate hostname/IP verification.
+ config.enable_sni = sni;
+ Arc::new(config)
+}
+
+struct Task(JoinHandle<()>);
+impl Drop for Task {
+ fn drop(&mut self) {
+ self.0.abort();
+ }
+}
+
+struct Proxy {
+ addr: SocketAddr,
+ _task: Task,
+}
+
+impl Proxy {
+ async fn start(policy: LocalExclusionStore) -> Self {
+ Self::with_doh(policy, DohConfig::default()).await
+ }
+
+ async fn with_doh(policy: LocalExclusionStore, doh_config: DohConfig) -> Self {
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let addr = listener.local_addr().unwrap();
+ let clients = UpstreamClients::with_test_roots(&[der(&pki().upstream.0)]);
+ let cache = CertCache::new(pki().proxy.0.clone(), pki().proxy.1.clone());
+ let blocker = AdblockRequester::new(BlockingDisabledStore(Arc::new(RwLock::new(false))));
+ let (events, _) = broadcast::channel(4);
+ let task = tokio::spawn(async move {
+ let mut sessions = JoinSet::new();
+ while let Ok((stream, peer)) = listener.accept().await {
+ let clients = clients.clone();
+ let cache = cache.clone();
+ let blocker = blocker.clone();
+ let events = events.clone();
+ let policy = policy.clone();
+ let doh_config = doh_config.clone();
+ sessions.spawn(async move {
+ let service = service_fn(move |request| {
+ serve_mitm_session(
+ blocker.clone(),
+ clients.clone(),
+ request,
+ cache.clone(),
+ events.clone(),
+ Statistics::new(),
+ peer.ip(),
+ policy.clone(),
+ doh_config.clone(),
+ false,
+ TlsFailureStore::new(Default::default()),
+ None,
+ UserScriptContext {
+ store: UserScriptStore::default(),
+ gm_storage: GmStorageStore::in_memory(),
+ endpoint_signing_key: "fixture-only".into(),
+ allow_private_network_requests: Default::default(),
+ },
+ )
+ });
+ let _ = auto::Builder::new(TokioExecutor::new())
+ .serve_connection_with_upgrades(TokioIo::new(stream), service)
+ .await;
+ });
+ }
+ });
+ Self {
+ addr,
+ _task: Task(task),
+ }
+ }
+
+ async fn connect(
+ &self,
+ destination: &str,
+ config: Arc,
+ name: &str,
+ ) -> TlsStream {
+ let mut tcp = TcpStream::connect(self.addr).await.unwrap();
+ tcp.write_all(
+ format!("CONNECT {destination} HTTP/1.1\r\nHost: {destination}\r\n\r\n").as_bytes(),
+ )
+ .await
+ .unwrap();
+ assert!(read_headers(&mut tcp)
+ .await
+ .unwrap()
+ .starts_with(b"HTTP/1.1 200"));
+ timeout(
+ TEST_TIMEOUT,
+ TlsConnector::from(config).connect(ServerName::try_from(name.to_owned()).unwrap(), tcp),
+ )
+ .await
+ .unwrap()
+ .unwrap()
+ }
+}
+
+// Read exactly through the header terminator, retaining TLS/DNS bytes that
+// might arrive in the same TCP packet.
+async fn read_headers(stream: &mut (impl AsyncRead + Unpin)) -> io::Result> {
+ let mut bytes = Vec::new();
+ while !bytes.ends_with(b"\r\n\r\n") {
+ bytes.push(stream.read_u8().await?);
+ assert!(bytes.len() < 32 * 1024);
+ }
+ Ok(bytes)
+}
+
+#[derive(Clone, Copy)]
+enum Protocol {
+ Http,
+ HttpKeepAlive,
+ HttpWaitForPeer,
+ Http2RefuseOnce,
+ Http2ResponseHeaders(usize),
+ Dns,
+ Upgrade,
+}
+
+struct Origin {
+ addr: SocketAddr,
+ requests: mpsc::UnboundedReceiver<(Option, Vec)>,
+ _task: Task,
+}
+
+impl Origin {
+ async fn start(configs: Vec<(&str, Arc)>, protocol: Protocol) -> Self {
+ Self::bind("127.0.0.1:0", configs, protocol).await
+ }
+
+ async fn bind(
+ address: &str,
+ configs: Vec<(&str, Arc)>,
+ protocol: Protocol,
+ ) -> Self {
+ let listener = TcpListener::bind(address).await.unwrap();
+ let addr = listener.local_addr().unwrap();
+ let fallback = configs[0].1.clone();
+ let configs: BTreeMap<_, _> = configs
+ .into_iter()
+ .map(|(name, config)| (name.to_owned(), config))
+ .collect();
+ let (tx, requests) = mpsc::unbounded_channel();
+ let response_gate = Arc::new(tokio::sync::Notify::new());
+ let task = tokio::spawn(async move {
+ let mut sessions = JoinSet::new();
+ while let Ok((tcp, _)) = listener.accept().await {
+ let configs = configs.clone();
+ let fallback = fallback.clone();
+ let tx = tx.clone();
+ let response_gate = response_gate.clone();
+ sessions.spawn(async move {
+ let Ok(hello) = LazyConfigAcceptor::new(Default::default(), tcp).await else { return; };
+ let name = hello.client_hello().server_name().map(str::to_owned);
+ let config = name.as_ref().and_then(|name| configs.get(name)).unwrap_or(&fallback).clone();
+ let Ok(mut tls) = hello.into_stream(config).await else { return; };
+ match protocol {
+ Protocol::Http | Protocol::HttpKeepAlive | Protocol::HttpWaitForPeer => {
+ let keep_alive = matches!(protocol, Protocol::HttpKeepAlive);
+ loop {
+ let Ok(request) = read_headers(&mut tls).await else { return; };
+ let slow = request.starts_with(b"GET /slow ");
+ let fast = request.starts_with(b"GET /fast ");
+ tx.send((name.clone(), request)).unwrap();
+ if matches!(protocol, Protocol::HttpWaitForPeer) {
+ if slow {
+ response_gate.notified().await;
+ } else if fast {
+ response_gate.notify_one();
+ }
+ }
+ let body = format!("upstream:{}", name.as_deref().unwrap_or("no-sni"));
+ let connection = if keep_alive { "keep-alive" } else { "close" };
+ tls.write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: {connection}\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap();
+ if !keep_alive { break; }
+ }
+ }
+ Protocol::Dns => {
+ // Two actual DNS-over-TCP frames, including their
+ // length prefixes and binary DNS response headers.
+ for _ in 0..2 {
+ let length = tls.read_u16().await.unwrap();
+ let mut query = vec![0; length as usize];
+ tls.read_exact(&mut query).await.unwrap();
+ tx.send((name.clone(), query.clone())).unwrap();
+ query[2..4].copy_from_slice(&[0x81, 0x83]); // NXDOMAIN
+ tls.write_u16(length).await.unwrap();
+ tls.write_all(&query).await.unwrap();
+ }
+ }
+ Protocol::Http2RefuseOnce => {
+ let mut connection = h2::server::handshake(tls).await.unwrap();
+ let mut refused = false;
+ while let Some(request) = connection.accept().await {
+ let (request, mut respond) = request.unwrap();
+ tx.send((name.clone(), request.method().as_str().as_bytes().to_vec())).unwrap();
+ if !refused {
+ // The origin explicitly guarantees it did
+ // not process this request. A bodyless GET
+ // should be replayed by reqwest's policy.
+ respond.send_reset(h2::Reason::REFUSED_STREAM);
+ refused = true;
+ } else {
+ let response = http::Response::builder()
+ .header("content-type", "text/plain")
+ .body(())
+ .unwrap();
+ respond.send_response(response, false).unwrap()
+ .send_data(bytes::Bytes::from_static(b"retried"), true).unwrap();
+ }
+ }
+ return;
+ }
+ Protocol::Http2ResponseHeaders(size) => {
+ let mut connection = h2::server::handshake(tls).await.unwrap();
+ while let Some(request) = connection.accept().await {
+ let (request, mut respond) = request.unwrap();
+ tx.send((name.clone(), request.method().as_str().as_bytes().to_vec())).unwrap();
+ let response = http::Response::builder()
+ .header("content-type", "text/plain")
+ .header("content-security-policy", format!("script-src https://{}.test", "a".repeat(size)))
+ .body(())
+ .unwrap();
+ respond.send_response(response, false).unwrap()
+ .send_data(bytes::Bytes::from_static(b"large headers"), true).unwrap();
+ }
+ return;
+ }
+ Protocol::Upgrade => {
+ let request = read_headers(&mut tls).await.unwrap();
+ tx.send((name, request)).unwrap();
+ tls.write_all(b"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n").await.unwrap();
+ let mut payload = [0; 4];
+ tls.read_exact(&mut payload).await.unwrap();
+ tls.write_all(&payload).await.unwrap();
+ }
+ }
+ let _ = tls.shutdown().await;
+ });
+ }
+ });
+ Self {
+ addr,
+ requests,
+ _task: Task(task),
+ }
+ }
+
+ async fn request(&mut self) -> (Option, Vec) {
+ timeout(TEST_TIMEOUT, self.requests.recv())
+ .await
+ .unwrap()
+ .unwrap()
+ }
+}
+
+async fn http_get(
+ stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static,
+) -> (http::StatusCode, String) {
+ http_request(stream, "https://unrelated.invalid:1234/test", "text/plain").await
+}
+
+async fn http_request(
+ stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static,
+ uri: &str,
+ accept: &str,
+) -> (http::StatusCode, String) {
+ // Neither an inner Host nor an absolute URL may change the selected peer.
+ let request = Request::builder()
+ .uri(uri)
+ .header("Host", "unrelated.invalid:1234")
+ .header("Accept", accept)
+ .body(empty_body())
+ .unwrap();
+ send_http_request(stream, request).await
+}
+
+async fn send_http_request(
+ stream: impl AsyncRead + AsyncWrite + Unpin + Send + 'static,
+ request: Request,
+) -> (http::StatusCode, String) {
+ let (mut sender, connection) = hyper::client::conn::http1::handshake(TokioIo::new(stream))
+ .await
+ .unwrap();
+ let _task = Task(tokio::spawn(async {
+ let _ = connection.await;
+ }));
+ let response = timeout(TEST_TIMEOUT, sender.send_request(request))
+ .await
+ .unwrap()
+ .unwrap();
+ let status = response.status();
+ let bytes = timeout(TEST_TIMEOUT, response.into_body().collect())
+ .await
+ .unwrap()
+ .unwrap()
+ .to_bytes();
+ (status, String::from_utf8(bytes.to_vec()).unwrap())
+}
+
+fn peer_certificate(tls: &TlsStream) -> X509 {
+ X509::from_der(tls.get_ref().1.peer_certificates().unwrap()[0].as_ref()).unwrap()
+}
+
+fn assert_dns_identity(cert: &X509, host: &str) {
+ let names = cert.subject_alt_names().unwrap();
+ assert_eq!(names.len(), 1);
+ assert_eq!(names[0].dnsname(), Some(host));
+ assert!(cert.verify(&pki().proxy.0.public_key().unwrap()).unwrap());
+}
+
+#[tokio::test]
+async fn ip_connect_uses_sni_for_certificates_upstream_tls_and_cache() {
+ let mut origin = Origin::start(
+ vec![
+ ("one.test", origin_config("one.test", true, false)),
+ ("two.test", origin_config("two.test", true, false)),
+ ],
+ Protocol::Http,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ let mut first_cert = None;
+ for host in ["one.test", "two.test", "one.test"] {
+ let tls = proxy
+ .connect(&origin.addr.to_string(), client_config(true, true), host)
+ .await;
+ let cert = peer_certificate(&tls);
+ assert_dns_identity(&cert, host);
+ if host == "one.test" {
+ if let Some(first) = &first_cert {
+ assert_eq!(&cert.to_der().unwrap(), first);
+ } else {
+ first_cert = Some(cert.to_der().unwrap());
+ }
+ }
+ let (status, body) = http_get(tls).await;
+ assert_eq!(status, http::StatusCode::OK, "{body}");
+ assert_eq!(body, format!("upstream:{host}"));
+ let (sni, request) = origin.request().await;
+ assert_eq!(sni.as_deref(), Some(host));
+ let request = String::from_utf8(request).unwrap().to_ascii_lowercase();
+ assert!(request.starts_with("get /test http/1.1"));
+ assert!(request.contains(&format!("host: {host}:{}\r\n", origin.addr.port())));
+ }
+}
+
+#[tokio::test]
+async fn same_sni_on_different_connect_destinations_does_not_reuse_the_wrong_pool() {
+ let mut first = Origin::start(
+ vec![("shared.test", origin_config("shared.test", true, false))],
+ Protocol::HttpKeepAlive,
+ )
+ .await;
+ let address = format!("127.0.0.2:{}", first.addr.port());
+ let mut second = Origin::bind(
+ &address,
+ vec![("shared.test", origin_config("shared.test", true, false))],
+ Protocol::HttpKeepAlive,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ for index in 0..3 {
+ let origin = if index == 1 { &mut second } else { &mut first };
+ let tls = proxy
+ .connect(
+ &origin.addr.to_string(),
+ client_config(true, true),
+ "shared.test",
+ )
+ .await;
+ assert_eq!(http_get(tls).await.0, http::StatusCode::OK);
+ assert_eq!(origin.request().await.0.as_deref(), Some("shared.test"));
+ }
+}
+
+#[tokio::test]
+async fn hostname_and_literal_ip_connect_keep_verification_and_missing_sni_behavior() {
+ for (connect_host, identity, send_sni) in [
+ ("localhost", "localhost", true),
+ ("localhost", "localhost", false),
+ ("127.0.0.1", "127.0.0.1", false),
+ ("[::1]", "::1", false),
+ ("[::1]", "one.test", true),
+ ] {
+ let bind = if connect_host == "[::1]" {
+ "[::1]:0"
+ } else {
+ "127.0.0.1:0"
+ };
+ let mut origin = Origin::bind(
+ bind,
+ vec![(identity, origin_config(identity, true, false))],
+ Protocol::Http,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ let authority = format!("{connect_host}:{}", origin.addr.port());
+ let tls = proxy
+ .connect(&authority, client_config(true, send_sni), identity)
+ .await;
+ let cert = peer_certificate(&tls);
+ if identity.parse::().is_err() {
+ assert_dns_identity(&cert, identity);
+ } else {
+ assert!(cert.subject_alt_names().unwrap()[0].ipaddress().is_some());
+ }
+ let (status, body) = http_get(tls).await;
+ assert_eq!(status, http::StatusCode::OK, "{body}");
+ let upstream_sni = origin.request().await.0;
+ assert_eq!(
+ upstream_sni.as_deref(),
+ if identity.parse::().is_err() {
+ Some(identity)
+ } else {
+ None
+ }
+ );
+ }
+}
+
+#[tokio::test]
+async fn ip_connect_without_sni_does_not_guess_a_dns_identity() {
+ let origin = Origin::start(
+ vec![("one.test", origin_config("one.test", true, false))],
+ Protocol::Http,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ let mut tcp = TcpStream::connect(proxy.addr).await.unwrap();
+ tcp.write_all(format!("CONNECT {} HTTP/1.1\r\n\r\n", origin.addr).as_bytes())
+ .await
+ .unwrap();
+ read_headers(&mut tcp).await.unwrap();
+ let result = timeout(
+ TEST_TIMEOUT,
+ TlsConnector::from(client_config(true, false))
+ .connect(ServerName::try_from("one.test").unwrap(), tcp),
+ )
+ .await
+ .unwrap();
+ assert!(
+ result.is_err(),
+ "a DNS verifier must not accept an IP-only certificate"
+ );
+}
+
+#[tokio::test]
+async fn sni_exclusions_and_ip_exclusions_bypass_interception() {
+ for (host, exclusions) in [
+ ("excluded.test", vec!["*.test".into()]),
+ ("www.apple.com", vec![]), // built-in exclusion from the reported failure
+ ("one.test", vec!["127.0.0.1".into()]),
+ ] {
+ let mut origin = Origin::start(
+ vec![(host, origin_config(host, true, false))],
+ Protocol::Http,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(exclusions)).await;
+ // Trust only the origin CA: a minted interception cert must fail here.
+ let tls = proxy
+ .connect(&origin.addr.to_string(), client_config(false, true), host)
+ .await;
+ assert!(peer_certificate(&tls)
+ .verify(&pki().upstream.0.public_key().unwrap())
+ .unwrap());
+ assert_eq!(http_get(tls).await.0, http::StatusCode::OK);
+ assert_eq!(origin.request().await.0.as_deref(), Some(host));
+ }
+}
+
+#[tokio::test]
+async fn include_only_rules_are_evaluated_after_ip_sni_and_exclusions_win() {
+ let origin = Origin::start(
+ vec![("included.test", origin_config("included.test", true, false))],
+ Protocol::Http,
+ )
+ .await;
+ for (exclusions, intercept) in [
+ (vec![], true),
+ (vec!["included.test".into()], false),
+ (vec!["127.0.0.1".into()], false),
+ ] {
+ let policy =
+ LocalExclusionStore::with_rules(true, vec!["included.test".into()], exclusions);
+ let proxy = Proxy::start(policy).await;
+ let tls = proxy
+ .connect(
+ &origin.addr.to_string(),
+ client_config(intercept, true),
+ "included.test",
+ )
+ .await;
+ if intercept {
+ assert_dns_identity(&peer_certificate(&tls), "included.test");
+ }
+ assert_eq!(http_get(tls).await.0, http::StatusCode::OK);
+ }
+}
+
+#[tokio::test]
+async fn browser_alpn_enables_parallel_http2_requests_and_keeps_http1() {
+ let host = "multiplex.test";
+ let mut origin = Origin::start(
+ vec![(host, origin_config(host, true, false))],
+ Protocol::HttpWaitForPeer,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ let mut config = (*client_config(true, true)).clone();
+ config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+ let tls = proxy
+ .connect(&origin.addr.to_string(), Arc::new(config), host)
+ .await;
+ assert_eq!(tls.get_ref().1.alpn_protocol(), Some(b"h2".as_slice()));
+ let (mut sender, connection) =
+ hyper::client::conn::http2::handshake(TokioExecutor::new(), TokioIo::new(tls))
+ .await
+ .unwrap();
+ let _task = Task(tokio::spawn(async {
+ let _ = connection.await;
+ }));
+
+ // The first origin response is held until the second request arrives.
+ // Both requests must share this one browser-side TLS connection.
+ let slow = tokio::spawn(
+ sender.send_request(
+ Request::builder()
+ .uri("https://unrelated.invalid/slow")
+ .body(empty_body())
+ .unwrap(),
+ ),
+ );
+ let (sni, request) = origin.request().await;
+ assert_eq!(sni.as_deref(), Some(host));
+ assert!(request.starts_with(b"GET /slow "));
+ let response = timeout(
+ TEST_TIMEOUT,
+ sender.send_request(
+ Request::builder()
+ .uri("https://unrelated.invalid/fast")
+ // Cookie-heavy browsers must not hit hyper's 16 KiB default
+ // when they start negotiating HTTP/2 with the proxy.
+ .header("cookie", format!("padding={}", "a".repeat(24 * 1024)))
+ .body(empty_body())
+ .unwrap(),
+ ),
+ )
+ .await
+ .unwrap()
+ .unwrap();
+ let slow_response = timeout(TEST_TIMEOUT, slow).await.unwrap().unwrap().unwrap();
+ for response in [response, slow_response] {
+ assert_eq!(response.status(), http::StatusCode::OK);
+ let body = timeout(TEST_TIMEOUT, response.into_body().collect())
+ .await
+ .unwrap()
+ .unwrap()
+ .to_bytes();
+ assert_eq!(body, format!("upstream:{host}"));
+ }
+ let (sni, request) = origin.request().await;
+ assert_eq!(sni.as_deref(), Some(host));
+ assert!(request.starts_with(b"GET /fast "));
+ assert!(String::from_utf8(request)
+ .unwrap()
+ .contains(&format!("host: {host}:{}", origin.addr.port())));
+
+ let mut config = (*client_config(true, true)).clone();
+ config.alpn_protocols = vec![b"http/1.1".to_vec()];
+ let tls = proxy
+ .connect(&origin.addr.to_string(), Arc::new(config), host)
+ .await;
+ assert_eq!(
+ tls.get_ref().1.alpn_protocol(),
+ Some(b"http/1.1".as_slice())
+ );
+ assert_eq!(http_get(tls).await.0, http::StatusCode::OK);
+}
+
+#[tokio::test]
+async fn empty_get_retries_a_refused_http2_stream() {
+ let mut config = (*origin_config("retry.test", true, false)).clone();
+ config.alpn_protocols = vec![b"h2".to_vec()];
+ let mut origin = Origin::start(
+ vec![("retry.test", Arc::new(config))],
+ Protocol::Http2RefuseOnce,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ let tls = proxy
+ .connect(
+ &origin.addr.to_string(),
+ client_config(true, true),
+ "retry.test",
+ )
+ .await;
+
+ let (status, body) = http_get(tls).await;
+ assert_eq!(status, http::StatusCode::OK, "{body}");
+ assert_eq!(body, "retried");
+ for _ in 0..2 {
+ let (sni, method) = origin.request().await;
+ assert_eq!(sni.as_deref(), Some("retry.test"));
+ assert_eq!(method, b"GET");
+ }
+ assert!(origin.requests.try_recv().is_err());
+}
+
+#[tokio::test]
+async fn nonempty_requests_are_not_replayed_after_http2_refusal() {
+ for method in [Method::GET, Method::POST] {
+ let mut config = (*origin_config("retry.test", true, false)).clone();
+ config.alpn_protocols = vec![b"h2".to_vec()];
+ let mut origin = Origin::start(
+ vec![("retry.test", Arc::new(config))],
+ Protocol::Http2RefuseOnce,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ let tls = proxy
+ .connect(
+ &origin.addr.to_string(),
+ client_config(true, true),
+ "retry.test",
+ )
+ .await;
+ let request = Request::builder()
+ .method(method.clone())
+ .uri("/upload")
+ .body(crate::proxy::full_body("must not be replayed"))
+ .unwrap();
+ let (status, _) = send_http_request(tls, request).await;
+ assert_eq!(status, http::StatusCode::BAD_GATEWAY);
+ assert_eq!(origin.request().await.1, method.as_str().as_bytes());
+ assert!(origin.requests.try_recv().is_err());
+ }
+}
+
+#[tokio::test]
+async fn large_http2_response_headers_are_accepted_with_a_bounded_limit() {
+ // Exercise the shared client (literal IP) and an SNI-pinned client. The
+ // decoded size, including HPACK's per-field overhead, matters here, not
+ // the compressed bytes on the wire.
+ for host in ["127.0.0.1", "headers.test"] {
+ for (size, expected) in [
+ (24 * 1024, http::StatusCode::OK),
+ (128 * 1024, http::StatusCode::BAD_GATEWAY),
+ ] {
+ let mut config = (*origin_config(host, true, false)).clone();
+ config.alpn_protocols = vec![b"h2".to_vec()];
+ let origin = Origin::start(
+ vec![(host, Arc::new(config))],
+ Protocol::Http2ResponseHeaders(size),
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ let tls = proxy
+ .connect(&origin.addr.to_string(), client_config(true, true), host)
+ .await;
+ let (status, body) = http_get(tls).await;
+ let reason = body
+ .lines()
+ .find(|line| line.contains("error sending request"))
+ .unwrap_or(&body);
+ assert_eq!(status, expected, "host={host}, size={size}: {reason}");
+ if expected == http::StatusCode::OK {
+ assert_eq!(body, "large headers");
+ }
+ }
+ }
+}
+
+#[tokio::test]
+async fn invalid_upstream_certificates_are_rejected() {
+ for (cert_host, trusted, expired) in [
+ ("one.test", false, false),
+ ("wrong.test", true, false),
+ ("one.test", true, true),
+ ] {
+ let mut origin = Origin::start(
+ vec![("one.test", origin_config(cert_host, trusted, expired))],
+ Protocol::Http,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ let tls = proxy
+ .connect(
+ &origin.addr.to_string(),
+ client_config(true, true),
+ "one.test",
+ )
+ .await;
+ let (status, body) = http_get(tls).await;
+ assert_eq!(
+ status,
+ http::StatusCode::BAD_GATEWAY,
+ "cert={cert_host}, trusted={trusted}, expired={expired}"
+ );
+ assert!(body.contains("invalid peer certificate"), "{body}");
+ assert!(
+ origin.requests.try_recv().is_err(),
+ "HTTP must never reach an unverified peer"
+ );
+ // The separate hyper client used for upgrades must reject it too.
+ let mut tls = proxy
+ .connect(
+ &origin.addr.to_string(),
+ client_config(true, true),
+ "one.test",
+ )
+ .await;
+ tls.write_all(b"GET /socket HTTP/1.1\r\nHost: one.test\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n").await.unwrap();
+ let response = timeout(TEST_TIMEOUT, read_headers(&mut tls))
+ .await
+ .unwrap()
+ .unwrap();
+ assert!(response.starts_with(b"HTTP/1.1 502"));
+ assert!(origin.requests.try_recv().is_err());
+ }
+}
+
+#[tokio::test]
+async fn ip_connect_protocol_upgrades_keep_the_destination_and_verify_sni() {
+ let mut origin = Origin::start(
+ vec![("upgrade.test", origin_config("upgrade.test", true, false))],
+ Protocol::Upgrade,
+ )
+ .await;
+ let proxy = Proxy::start(LocalExclusionStore::new(vec![])).await;
+ let mut tls = proxy
+ .connect(
+ &origin.addr.to_string(),
+ client_config(true, true),
+ "upgrade.test",
+ )
+ .await;
+ tls.write_all(b"GET /socket HTTP/1.1\r\nHost: unrelated.invalid\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n").await.unwrap();
+ let response = timeout(TEST_TIMEOUT, read_headers(&mut tls))
+ .await
+ .unwrap()
+ .unwrap();
+ assert!(response.starts_with(b"HTTP/1.1 101"));
+ tls.write_all(b"ping").await.unwrap();
+ let mut echo = [0; 4];
+ timeout(TEST_TIMEOUT, tls.read_exact(&mut echo))
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(&echo, b"ping");
+ let (sni, request) = origin.request().await;
+ assert_eq!(sni.as_deref(), Some("upgrade.test"));
+ assert!(String::from_utf8(request)
+ .unwrap()
+ .contains(&format!("host: upgrade.test:{}", origin.addr.port())));
+}
+
+#[tokio::test]
+async fn dns_over_tls_passes_binary_queries_and_responses_unchanged() {
+ let mut origin = Origin::start(
+ vec![("dns.quad9.net", origin_config("dns.quad9.net", true, false))],
+ Protocol::Dns,
+ )
+ .await;
+ // An ephemeral unprivileged port keeps this test runnable without root.
+ // Standard port 853's automatic bypass is covered separately below.
+ let proxy = Proxy::start(LocalExclusionStore::new(vec!["dns.quad9.net".into()])).await;
+ let mut tls = proxy
+ .connect(
+ &origin.addr.to_string(),
+ client_config(false, true),
+ "dns.quad9.net",
+ )
+ .await;
+ for id in [0x12, 0x34] {
+ let query = [
+ id, 0x56, 0x01, 0x00, 0, 1, 0, 0, 0, 0, 0, 0, 7, b'e', b'x', b'a', b'm', b'p', b'l',
+ b'e', 3, b'c', b'o', b'm', 0, 0, 1, 0, 1,
+ ];
+ tls.write_u16(query.len() as u16).await.unwrap();
+ tls.write_all(&query).await.unwrap();
+ let length = timeout(TEST_TIMEOUT, tls.read_u16())
+ .await
+ .unwrap()
+ .unwrap();
+ let mut response = vec![0; length as usize];
+ timeout(TEST_TIMEOUT, tls.read_exact(&mut response))
+ .await
+ .unwrap()
+ .unwrap();
+ let mut expected = query;
+ expected[2..4].copy_from_slice(&[0x81, 0x83]);
+ assert_eq!(response, expected);
+ let (sni, received) = origin.request().await;
+ assert_eq!(sni.as_deref(), Some("dns.quad9.net"));
+ assert_eq!(received, query);
+ }
+}
+
+#[test]
+fn standard_dot_port_bypasses_without_consuming_tls_or_entering_http() {
+ let policy = LocalExclusionStore::new(vec![]);
+ for destination in ["9.9.9.9:853", "[2620:fe::fe]:853", "dns.quad9.net:853"] {
+ let authority: Authority = destination.parse().unwrap();
+ assert!(bypass_before_client_hello(&policy, &authority, &authority));
+ }
+ let https: Authority = "9.9.9.9:443".parse().unwrap();
+ assert!(!bypass_before_client_hello(&policy, &https, &https));
+}
+
+fn initial_client_hello(
+ config: Arc,
+ host: &str,
+) -> (rustls::ClientConnection, Vec) {
+ let mut client =
+ rustls::ClientConnection::new(config, ServerName::try_from(host.to_owned()).unwrap())
+ .unwrap();
+ let mut bytes = Vec::new();
+ client.write_tls(&mut bytes).unwrap();
+ (client, bytes)
+}
+
+/// Split the handshake itself across TLS records as well as TCP writes. Just
+/// fragmenting TCP would miss implementations that only parse the first record.
+fn fragment_records(wire: &[u8]) -> Vec {
+ let mut fragmented = Vec::new();
+ let mut offset = 0;
+ while offset < wire.len() {
+ let header = &wire[offset..offset + 5];
+ let length = u16::from_be_bytes([header[3], header[4]]) as usize;
+ for chunk in wire[offset + 5..offset + 5 + length].chunks(17) {
+ fragmented.extend_from_slice(&header[..3]);
+ fragmented.extend_from_slice(&(chunk.len() as u16).to_be_bytes());
+ fragmented.extend_from_slice(chunk);
+ }
+ offset += 5 + length;
+ }
+ fragmented
+}
+
+async fn flush_tls(client: &mut rustls::ClientConnection, tcp: &mut TcpStream) {
+ while client.wants_write() {
+ let mut bytes = Vec::new();
+ client.write_tls(&mut bytes).unwrap();
+ tcp.write_all(&bytes).await.unwrap();
+ }
+}
+
+async fn receive_tls(client: &mut rustls::ClientConnection, tcp: &mut TcpStream) {
+ let mut bytes = [0; 16 * 1024];
+ let length = tcp.read(&mut bytes).await.unwrap();
+ assert_ne!(length, 0, "TLS peer closed unexpectedly");
+ let mut reader = &bytes[..length];
+ while !reader.is_empty() {
+ assert_ne!(client.read_tls(&mut reader).unwrap(), 0);
+ client.process_new_packets().unwrap();
+ }
+}
+
+#[tokio::test]
+async fn fragmented_client_hello_and_connect_read_ahead_work_for_mitm_and_bypass() {
+ for intercept in [true, false] {
+ let mut origin = Origin::start(
+ vec![("fragment.test", origin_config("fragment.test", true, false))],
+ Protocol::Http,
+ )
+ .await;
+ let policy = LocalExclusionStore::new(if intercept {
+ vec![]
+ } else {
+ vec!["fragment.test".into()]
+ });
+ let proxy = Proxy::start(policy).await;
+ timeout(TEST_TIMEOUT, async {
+ let (mut client, hello) =
+ initial_client_hello(client_config(intercept, true), "fragment.test");
+ let hello = fragment_records(&hello);
+ let mut tcp = TcpStream::connect(proxy.addr).await.unwrap();
+ tcp.set_nodelay(true).unwrap();
+ let mut request = format!(
+ "CONNECT {} HTTP/1.1\r\nHost: {}\r\n\r\n",
+ origin.addr, origin.addr
+ )
+ .into_bytes();
+ // Exercise hyper's upgrade read-ahead with a partial TLS header.
+ request.extend_from_slice(&hello[..3]);
+ tcp.write_all(&request).await.unwrap();
+ assert!(read_headers(&mut tcp)
+ .await
+ .unwrap()
+ .starts_with(b"HTTP/1.1 200"));
+ for chunk in hello[3..].chunks(3) {
+ tcp.write_all(chunk).await.unwrap();
+ tokio::task::yield_now().await;
+ }
+ while client.is_handshaking() {
+ flush_tls(&mut client, &mut tcp).await;
+ receive_tls(&mut client, &mut tcp).await;
+ }
+ if intercept {
+ let cert = X509::from_der(client.peer_certificates().unwrap()[0].as_ref()).unwrap();
+ assert_dns_identity(&cert, "fragment.test");
+ }
+ std::io::Write::write_all(
+ &mut client.writer(),
+ b"GET /fragment HTTP/1.1\r\nHost: fragment.test\r\nConnection: close\r\n\r\n",
+ )
+ .unwrap();
+ flush_tls(&mut client, &mut tcp).await;
+ let mut response = Vec::new();
+ loop {
+ receive_tls(&mut client, &mut tcp).await;
+ match std::io::Read::read_to_end(&mut client.reader(), &mut response) {
+ Ok(_) => {}
+ Err(error) if error.kind() == io::ErrorKind::WouldBlock => {}
+ Err(error) => panic!("TLS read: {error}"),
+ }
+ if response.ends_with(b"upstream:fragment.test") {
+ break;
+ }
+ }
+ assert!(response.starts_with(b"HTTP/1.1 200"));
+ assert_eq!(origin.request().await.0.as_deref(), Some("fragment.test"));
+ })
+ .await
+ .unwrap();
+ }
+}
+
+#[tokio::test]
+async fn client_hello_parser_retains_every_byte_at_all_record_and_buffer_boundaries() {
+ let (_, hello) = initial_client_hello(client_config(true, true), "fragment.test");
+ let mut wire = fragment_records(&hello);
+ // Bytes after the ClientHello may already be buffered, including TLS 1.3
+ // compatibility CCS or early data. A tunnel must preserve them verbatim.
+ wire.extend_from_slice(b"\x14\x03\x03\x00\x01\x01buffered-after-hello");
+ for capacity in [1, 2, 5, 19, 8192] {
+ let (mut writer, reader) = tokio::io::duplex(wire.len());
+ writer.write_all(&wire).await.unwrap();
+ writer.shutdown().await.unwrap();
+ let (mut hello, mut replay) = read_client_hello(BufReader::with_capacity(capacity, reader))
+ .await
+ .unwrap();
+ assert_eq!(hello.client_hello().server_name(), Some("fragment.test"));
+ hello.io.read_to_end(&mut replay).await.unwrap();
+ assert_eq!(replay, wire, "buffer capacity {capacity}");
+ }
+}
+
+#[tokio::test]
+async fn malformed_truncated_and_oversized_client_hellos_fail_closed() {
+ let mut oversized = vec![0x16, 3, 1, 0, 4, 1, 1, 0, 1];
+ for _ in 0..5 {
+ oversized.extend_from_slice(&[0x16, 3, 1, 0x40, 0]);
+ oversized.extend_from_slice(&[0; 16 * 1024]);
+ }
+ for bytes in [
+ b"\x16\x03\x01\x00\x20\x01".to_vec(), // truncated handshake
+ b"\x16\x03\x01\x00\x05\x02\x00\x00\x01\x00".to_vec(), // not ClientHello
+ oversized,
+ ] {
+ let (mut writer, reader) = tokio::io::duplex(bytes.len());
+ writer.write_all(&bytes).await.unwrap();
+ writer.shutdown().await.unwrap();
+ assert!(read_client_hello(BufReader::new(reader)).await.is_err());
+ }
+}
+
+#[test]
+fn tls_identity_only_uses_dns_sni_for_ip_connect_and_keeps_the_port() {
+ for (connect, sni, expected) in [
+ (
+ "192.0.2.1:4443",
+ Some("WWW.Example.test"),
+ "www.example.test:4443",
+ ),
+ ("[2001:db8::1]:443", Some("one.test"), "one.test:443"),
+ ("192.0.2.1:443", None, "192.0.2.1:443"),
+ ("[2001:db8::1]:443", Some("192.0.2.2"), "[2001:db8::1]:443"),
+ ("192.0.2.1:443", Some("invalid/host"), "192.0.2.1:443"),
+ (
+ "original.test:443",
+ Some("different.test"),
+ "original.test:443",
+ ),
+ ] {
+ assert_eq!(
+ tls_identity(&connect.parse().unwrap(), sni).as_str(),
+ expected
+ );
+ }
+}
+
+#[tokio::test]
+async fn connect_ip_pin_does_not_override_configured_doh_redirect_routing() {
+ let mut resolver = Origin::start(
+ vec![("localhost", origin_config("localhost", true, false))],
+ Protocol::Http,
+ )
+ .await;
+ let mut destination = Origin::bind(
+ &format!("127.0.0.2:{}", resolver.addr.port()),
+ vec![("localhost", origin_config("localhost", true, false))],
+ Protocol::Http,
+ )
+ .await;
+ let doh = DohConfig {
+ mode: crate::configuration::DohMode::Redirect,
+ upstream: Some(format!(
+ "https://localhost:{}/dns-query",
+ resolver.addr.port()
+ )),
+ extra_hosts: vec![],
+ };
+ let proxy = Proxy::with_doh(LocalExclusionStore::new(vec![]), doh).await;
+ let tls = proxy
+ .connect(
+ &destination.addr.to_string(),
+ client_config(true, true),
+ "localhost",
+ )
+ .await;
+ let (status, body) = http_request(tls, "/dns-query?dns=AA", "application/dns-message").await;
+ assert_eq!(status, http::StatusCode::OK, "{body}");
+ let (sni, request) = resolver.request().await;
+ assert_eq!(sni.as_deref(), Some("localhost"));
+ assert!(request.starts_with(b"GET /dns-query?dns=AA HTTP/1.1"));
+ assert!(
+ destination.requests.try_recv().is_err(),
+ "redirect must resolve its own destination"
+ );
+}
diff --git a/privaxy/src/server/proxy/exclusions.rs b/privaxy/src/server/proxy/exclusions.rs
index c6aeb10b..3d2056f2 100644
--- a/privaxy/src/server/proxy/exclusions.rs
+++ b/privaxy/src/server/proxy/exclusions.rs
@@ -298,12 +298,30 @@ impl From<&crate::configuration::Configuration> for InterceptionRules {
pub struct LocalExclusionStore(Arc>);
impl LocalExclusionStore {
+ /// Unlike an include-only miss, an explicit exclusion can be honored
+ /// before looking for SNI on an IP CONNECT (including server-first flows).
+ pub fn is_excluded(&self, host: &str, raw_host: &str) -> bool {
+ let rules = self.0.read().unwrap();
+ [host, raw_host]
+ .iter()
+ .any(|host| DEFAULT_EXCLUSIONS.is_match(host) || rules.exclusions.is_match(host))
+ }
+
#[cfg(test)]
pub fn new(exclusions: Vec) -> Self {
+ Self::with_rules(false, Vec::new(), exclusions)
+ }
+
+ #[cfg(test)]
+ pub(super) fn with_rules(
+ include_only: bool,
+ inclusions: Vec,
+ exclusions: Vec,
+ ) -> Self {
Self(Arc::new(RwLock::new(InterceptionRules {
exclusions: WildMatchCollection::new(exclusions),
- inclusions: WildMatchCollection::new(Vec::new()),
- include_only: false,
+ inclusions: WildMatchCollection::new(inclusions),
+ include_only,
})))
}
diff --git a/privaxy/src/server/proxy/html_rewriter.rs b/privaxy/src/server/proxy/html_rewriter.rs
index d3cfd65a..8173f775 100644
--- a/privaxy/src/server/proxy/html_rewriter.rs
+++ b/privaxy/src/server/proxy/html_rewriter.rs
@@ -216,8 +216,11 @@ impl Rewriter {
// silently no-op. Defining it once at the top of the payload (the
// scriptlet bodies use dot-access, so it must be an object)
// so the scriptlets actually work.
- payload.push_str("const scriptletGlobals = {};\n");
+ // Function-style resources share helpers with one another, but must
+ // not publish/overwrite names such as safeSelf or JSONPath on the page.
+ payload.push_str("(() => {\nconst scriptletGlobals = {};\n");
payload.push_str(&script);
+ payload.push_str("\n})();\n");
}
if shim_needed {
@@ -427,6 +430,10 @@ impl Rewriter {
endpoint_tokens,
} = self;
+ // Head scriptlets and the trailing cosmetic stylesheet belong to one
+ // response. Their separate tasks share a single counting guard.
+ let statistics = statistics.for_response();
+
let CosmeticBlockerResult {
hidden_selectors,
style_selectors,
@@ -676,6 +683,56 @@ impl Rewriter {
#[cfg(test)]
mod tests {
use super::*;
+
+ #[tokio::test]
+ async fn head_and_body_injections_count_each_response_only_once() {
+ use crate::blocker::BlockingDisabledStore;
+ use http_body_util::BodyExt;
+ use std::sync::RwLock;
+
+ let statistics = Statistics::new();
+ for expected in 1..=2 {
+ let requester =
+ AdblockRequester::new(BlockingDisabledStore(Arc::new(RwLock::new(false))));
+ let mut cosmetics = requester
+ .get_cosmetic_response("https://example.test/".into())
+ .await;
+ cosmetics.hidden_selectors.push(".ad-test".into());
+ let (input, receiver) = mpsc::channel(2);
+ let (sender, mut body) = body_channel();
+ let rewriter = Rewriter::new(
+ requester,
+ receiver,
+ sender,
+ statistics.clone(),
+ "fixture".into(),
+ cosmetics,
+ false,
+ Vec::new(),
+ GmStorageStore::in_memory(),
+ HashMap::new(),
+ );
+ input
+ .send(Ok(Bytes::from_static(
+ b"
ad
",
+ )))
+ .await
+ .unwrap();
+ drop(input);
+ let task = tokio::task::spawn_blocking(move || rewriter.rewrite());
+ let mut output = Vec::new();
+ while let Some(frame) = body.frame().await {
+ if let Ok(bytes) = frame.unwrap().into_data() {
+ output.extend_from_slice(&bytes);
+ }
+ }
+ task.await.unwrap();
+ let html = String::from_utf8(output).unwrap();
+ assert!(html.contains("