test: merge pr-300

This commit is contained in:
SGavrl committed 2026-06-17 14:33:37 +00:00
commit b27318dc78
18 files changed
+1291 -425

No files matched your search

+19 -23
View File
@@ -20,30 +20,26 @@ jobs:
# newer glibc shipped by ubuntu-latest.
os: ubuntu-22.04
name: obscura-x86_64-linux
cross: false
- target: aarch64-unknown-linux-gnu
# Native build on GitHub's ARM runner.
os: ubuntu-22.04-arm
name: obscura-aarch64-linux
cross: false
- target: aarch64-unknown-linux-gnu
# Cross-compile fallback from x86_64 Ubuntu 22.04. Same glibc
# 2.35 floor, useful when ARM runners are unavailable.
os: ubuntu-22.04
name: obscura-aarch64-linux-cross
cross: true
- target: aarch64-apple-darwin
os: macos-latest
name: obscura-aarch64-macos
cross: false
- target: x86_64-apple-darwin
os: macos-latest
# Must run on a native Intel runner. obscura-js bakes a V8 startup
# snapshot at build time, and that snapshot is architecture specific.
# macos-latest is Apple Silicon, so building x86_64 there embeds an
# arm64 snapshot into the Intel binary and V8 crashes deserializing
# it at isolate init (issue #290). macos-15-intel is the last x86_64
# image (available until Aug 2027); there is no cross-build that
# produces a correct snapshot.
os: macos-15-intel
name: obscura-x86_64-macos
cross: false
- target: x86_64-pc-windows-msvc
os: windows-latest
name: obscura-x86_64-windows
cross: false
runs-on: ${{ matrix.os }}
@@ -54,17 +50,6 @@ jobs:
with:
targets: ${{ matrix.target }}
- name: Install cross-compilation toolchain (aarch64-linux)
if: matrix.cross == true
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
mkdir -p .cargo
cat >> .cargo/config.toml << 'CARGO_EOF'
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
CARGO_EOF
- name: Build
run: cargo build --release --target ${{ matrix.target }}
@@ -72,6 +57,17 @@ jobs:
run: cargo build --release --target ${{ matrix.target }} --features stealth
continue-on-error: true
- name: Smoke test (V8 snapshot)
# Run the V8 path once so a release never ships a binary that crashes at
# isolate init from a mismatched snapshot (issue #290). The data: URL
# keeps it offline. Every target here is native, so the runner can
# execute what it just built.
shell: bash
run: |
BIN=target/${{ matrix.target }}/release/obscura
[ -f "$BIN.exe" ] && BIN="$BIN.exe"
"$BIN" fetch "data:text/html,<title>ok</title>" --eval "1+1"
- name: Package (Unix)
if: runner.os != 'Windows'
run: |
Generated
+140 -229
View File
@@ -8,18 +8,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -44,6 +32,12 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstream"
version = "1.0.0"
@@ -221,9 +215,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "2.12.1"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "bitvec"
@@ -246,31 +240,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "boring-sys2"
version = "5.0.0-alpha.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455d79965f5155dcc88a7abce112c3590883889131b799beda10bf9a813ed669"
dependencies = [
"bindgen 0.72.1",
"cmake",
"fs_extra",
"fslock",
]
[[package]]
name = "boring2"
version = "5.0.0-alpha.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183ccc3854411c035410dcdbffafca62084f3a6c33f013c77e83c025d2a08a28"
dependencies = [
"bitflags",
"boring-sys2",
"foreign-types",
"libc",
"openssl-macros",
]
[[package]]
name = "brotli"
version = "8.0.3"
@@ -292,6 +261,31 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "btls"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c5e60b8c8d282c86360cab651ded04ab0335a7b5390c8d34145cbeab8cacf5f"
dependencies = [
"bitflags",
"btls-sys",
"foreign-types",
"libc",
"openssl-macros",
]
[[package]]
name = "btls-sys"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b1b8638a2e1c38a5ae4efa90ae57e643baec35a30d03fc5b399b893adc4954b"
dependencies = [
"bindgen 0.72.1",
"cmake",
"fs_extra",
"fslock",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -332,9 +326,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.63"
version = "1.2.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -453,35 +447,6 @@ version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "cookie_store"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
dependencies = [
"cookie",
"document-features",
"idna",
"log",
"publicsuffix",
"serde",
"serde_derive",
"serde_json",
"time",
"url",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -659,15 +624,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]]
name = "derive_more"
version = "0.99.20"
@@ -700,15 +656,6 @@ dependencies = [
"syn",
]
[[package]]
name = "document-features"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
dependencies = [
"litrs",
]
[[package]]
name = "dtoa"
version = "1.0.11"
@@ -789,6 +736,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -1019,19 +972,13 @@ dependencies = [
"crc32fast",
]
[[package]]
name = "hashbrown"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
"foldhash 0.1.5",
]
[[package]]
@@ -1039,6 +986,11 @@ name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "heck"
@@ -1069,9 +1021,9 @@ dependencies = [
[[package]]
name = "http"
version = "1.4.1"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
dependencies = [
"bytes",
"itoa",
@@ -1102,9 +1054,9 @@ dependencies = [
[[package]]
name = "http2"
version = "0.5.17"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "569ef7a780e853c4e1768f58a3c8168193b82cdcbab66638a0b1c6583ec5995e"
checksum = "9d8c910a959d6fd27dd6f8fdb26509ac0f6561465cc8c5f875d45a203ab97911"
dependencies = [
"atomic-waker",
"bytes",
@@ -1113,7 +1065,6 @@ dependencies = [
"futures-sink",
"http",
"indexmap",
"parking_lot",
"slab",
"smallvec",
"tokio",
@@ -1351,13 +1302,12 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.99"
version = "0.3.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
@@ -1407,12 +1357,6 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
name = "litrs"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
version = "0.4.14"
@@ -1428,6 +1372,15 @@ version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
[[package]]
name = "lru"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9"
dependencies = [
"hashbrown 0.17.1",
]
[[package]]
name = "lru-slab"
version = "0.1.2"
@@ -1476,9 +1429,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.8.1"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "minimal-lexical"
@@ -1543,12 +1496,6 @@ dependencies = [
"rand 0.8.6",
]
[[package]]
name = "num-conv"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-integer"
version = "0.1.46"
@@ -1864,12 +1811,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1927,22 +1868,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "psl-types"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "publicsuffix"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf"
dependencies = [
"idna",
"psl-types",
]
[[package]]
name = "quinn"
version = "0.11.9"
@@ -2080,9 +2005,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.12.3"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
@@ -2103,9 +2028,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.10"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "reqwest"
@@ -2115,8 +2040,6 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"cookie",
"cookie_store",
"futures-core",
"http",
"http-body",
@@ -2240,17 +2163,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "schnellru"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649"
dependencies = [
"ahash",
"cfg-if",
"hashbrown 0.13.2",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@@ -2434,9 +2346,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
name = "socket2"
@@ -2672,37 +2584,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -2746,12 +2627,12 @@ dependencies = [
]
[[package]]
name = "tokio-boring2"
version = "5.0.0-alpha.13"
name = "tokio-btls"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f81df1210d791f31d72d840de8fbd80b9c3cb324956523048b1413e2bd55756"
checksum = "2e1fd638ec35427faf3b8f412e0fdd6fae76591d79dba40f38fa667d22bc44dd"
dependencies = [
"boring2",
"btls",
"tokio",
]
@@ -3030,9 +2911,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.23.2"
version = "1.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@@ -3090,9 +2971,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
version = "1.0.4+wasi-0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
dependencies = [
"wit-bindgen 0.57.1",
]
@@ -3108,9 +2989,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.122"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
dependencies = [
"cfg-if",
"once_cell",
@@ -3121,9 +3002,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.72"
version = "0.4.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f"
checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3131,9 +3012,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.122"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3141,9 +3022,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.122"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -3154,9 +3035,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.122"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
dependencies = [
"unicode-ident",
]
@@ -3207,9 +3088,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.99"
version = "0.3.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3550,16 +3431,13 @@ dependencies = [
[[package]]
name = "wreq"
version = "6.0.0-rc.28"
version = "6.0.0-rc.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79937f6c4df65b3f6f78715b9de2977afe9ee3b3436483c7949a24511e25935"
checksum = "3f0eba5f5814a94e5f1a99156f187133464e525b66bdbc69a9627d46530af2e1"
dependencies = [
"ahash",
"boring2",
"brotli",
"btls",
"btls-sys",
"bytes",
"flate2",
"futures-channel",
"futures-util",
"http",
"http-body",
@@ -3568,29 +3446,62 @@ dependencies = [
"httparse",
"ipnet",
"libc",
"lru",
"percent-encoding",
"pin-project-lite",
"schnellru",
"smallvec",
"socket2",
"tokio",
"tokio-boring2",
"tokio-btls",
"tokio-socks",
"tower",
"url",
"want",
"webpki-root-certs",
"zstd",
"wreq-proto",
"wreq-rt",
]
[[package]]
name = "wreq-proto"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a43942f024bb303f1042c9aa3c87fa1d9149f507c65db6e5220a11ccdb207387"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"http2",
"httparse",
"pin-project-lite",
"smallvec",
"tokio",
"tokio-util",
"want",
]
[[package]]
name = "wreq-rt"
version = "0.2.2-rc.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99e9bce67a3fa3dd3f1503f066d86661c9caf399a763d3bd184da7afaf886c8b"
dependencies = [
"pin-project-lite",
"tokio",
"wreq-proto",
]
[[package]]
name = "wreq-util"
version = "3.0.0-rc.10"
version = "3.0.0-rc.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c6bbe24d28beb9ceb58b514bd6a613c759d3b706f768b9d2950d5d35b543c04"
checksum = "baa5d2ab72139256916ca352a3d05c53d74e1dd360052eb5ba7691033c417c65"
dependencies = [
"brotli",
"flate2",
"typed-builder",
"wreq",
"zstd",
]
[[package]]
@@ -3633,18 +3544,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.50"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.50"
version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
dependencies = [
"proc-macro2",
"quote",
@@ -3674,9 +3585,9 @@ dependencies = [
[[package]]
name = "zeroize"
version = "1.8.2"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
+5 -1
View File
@@ -32,7 +32,11 @@ servo_arc = "0.4"
cssparser = "0.34"
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.26"
reqwest = { version = "0.12", features = ["cookies", "gzip", "brotli", "deflate", "rustls-tls", "socks"], default-features = false }
# No "cookies" feature: obscura manages cookies through its own CookieJar
# (obscura-net/src/cookies.rs) and never uses reqwest's cookie store. Pulling
# it in only added the cookie/cookie_store crates, and cookie 0.18.1's blanket
# From impl fails to compile under coherence on some toolchains (issue #295).
reqwest = { version = "0.12", features = ["gzip", "brotli", "deflate", "rustls-tls", "socks"], default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
url = "2"
+128 -14
View File
@@ -140,6 +140,12 @@ pub struct NetworkEvent {
pub timestamp: f64,
}
#[derive(Debug, Clone)]
pub struct StoredResponseBody {
pub body: String,
pub base64_encoded: bool,
}
pub struct Page {
pub id: String,
pub frame_id: String,
@@ -161,6 +167,8 @@ pub struct Page {
pub history: Vec<String>,
pub history_index: usize,
pub network_events: Vec<NetworkEvent>,
response_bodies: std::collections::HashMap<String, StoredResponseBody>,
response_body_order: std::collections::VecDeque<String>,
network_event_counter: u32,
pub intercept_enabled: bool,
pub intercept_block_patterns: Vec<String>,
@@ -213,6 +221,8 @@ impl Page {
history: Vec::new(),
history_index: 0,
network_events: Vec::new(),
response_bodies: std::collections::HashMap::new(),
response_body_order: std::collections::VecDeque::new(),
network_event_counter: 0,
intercept_enabled: false,
intercept_block_patterns: Vec::new(),
@@ -275,19 +285,32 @@ impl Page {
#[cfg(feature = "stealth")]
if self.stealth_client.is_some() {
rt.set_stealth(true);
rt.set_user_agent(obscura_net::STEALTH_USER_AGENT);
} else if let Ok(ua) = self.http_client.user_agent.try_read() {
rt.set_user_agent(&ua);
// Match platform to the Linux UA so navigator.platform and
// userAgentData.platform are consistent with navigator.userAgent.
rt.set_platform("Linux x86_64", "Linux", "");
} else {
if let Ok(ua) = self.http_client.user_agent.try_read() {
rt.set_user_agent(&ua);
}
rt.set_platform(
&self.context.platform,
&self.context.ua_platform,
&self.context.ua_platform_version,
);
}
#[cfg(not(feature = "stealth"))]
if let Ok(ua) = self.http_client.user_agent.try_read() {
rt.set_user_agent(&ua);
{
if let Ok(ua) = self.http_client.user_agent.try_read() {
rt.set_user_agent(&ua);
}
rt.set_platform(
&self.context.platform,
&self.context.ua_platform,
&self.context.ua_platform_version,
);
}
rt.set_platform(
&self.context.platform,
&self.context.ua_platform,
&self.context.ua_platform_version,
);
if let Some((lat, lon)) = env_geolocation() {
rt.set_geolocation(lat, lon);
}
@@ -303,6 +326,8 @@ impl Page {
rt.set_dom(dom);
}
rt.run_page_init();
self.js = Some(rt);
}
@@ -576,7 +601,7 @@ impl Page {
if script.src.is_some() {
if let Some((url, code, resp)) = fetched.remove(&i) {
tracing::info!("Executing script ({} bytes): {}", code.len(), url);
self.record_network_event(&url, "GET", "Script", resp.status, &resp.headers, resp.body.len());
self.record_network_event_with_body(&url, "GET", "Script", resp.status, &resp.headers, &resp.body, false);
if let Some(js) = &mut self.js {
let _ = js.execute_script("<current-script>", &format!("globalThis.__currentScriptNid={};", script.nid));
if let Err(e) = js.execute_script_guarded(&url, &code) {
@@ -948,13 +973,14 @@ impl Page {
PageError::NetworkError(e.to_string())
})?;
self.record_network_event(
self.record_network_event_with_body(
url.as_str(),
"GET",
"Document",
response.status,
&response.headers,
response.body.len(),
&response.body,
false,
);
if !response.redirected_from.is_empty() {
@@ -1048,7 +1074,7 @@ impl Page {
// CSS bodies: honor the Content-Type charset; CSS @charset is
// out of scope for the current scrape-focused pipeline.
let css = obscura_net::decode_non_html(&resp.body, resp.content_type());
self.record_network_event(&url_str, "GET", "Stylesheet", resp.status, &resp.headers, resp.body.len());
self.record_network_event_with_body(&url_str, "GET", "Stylesheet", resp.status, &resp.headers, &resp.body, false);
css_sources.push(css);
}
}
@@ -1341,13 +1367,47 @@ impl Page {
response_headers: &std::collections::HashMap<String, String>,
body_size: usize,
) {
self.record_network_event_inner(url, method, resource_type, status, response_headers, body_size);
}
fn record_network_event_with_body(
&mut self,
url: &str,
method: &str,
resource_type: &str,
status: u16,
response_headers: &std::collections::HashMap<String, String>,
body: &[u8],
base64_encoded: bool,
) {
let request_id = self.record_network_event_inner(
url,
method,
resource_type,
status,
response_headers,
body.len(),
);
self.store_response_body(request_id, body, base64_encoded);
}
fn record_network_event_inner(
&mut self,
url: &str,
method: &str,
resource_type: &str,
status: u16,
response_headers: &std::collections::HashMap<String, String>,
body_size: usize,
) -> String {
self.network_event_counter += 1;
let request_id = format!("{}.{}", self.id, self.network_event_counter);
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64();
self.network_events.push(NetworkEvent {
request_id: format!("{}.{}", self.id, self.network_event_counter),
request_id: request_id.clone(),
url: url.to_string(),
method: method.to_string(),
resource_type: resource_type.to_string(),
@@ -1357,6 +1417,46 @@ impl Page {
body_size,
timestamp,
});
request_id
}
fn store_response_body(&mut self, request_id: String, body: &[u8], base64_encoded: bool) {
let max_entries = response_body_entry_limit();
let max_bytes = response_body_byte_limit();
if max_entries == 0 || max_bytes == 0 || body.len() > max_bytes {
return;
}
let body = if base64_encoded {
BASE64.encode(body)
} else {
String::from_utf8_lossy(body).to_string()
};
self.response_bodies.insert(request_id.clone(), StoredResponseBody { body, base64_encoded });
self.response_body_order.push_back(request_id);
while self.response_body_order.len() > max_entries {
if let Some(oldest) = self.response_body_order.pop_front() {
self.response_bodies.remove(&oldest);
}
}
}
pub fn get_response_body(&self, request_id: &str) -> Option<StoredResponseBody> {
self.response_bodies.get(request_id).cloned().or_else(|| {
self.js.as_ref()?.get_network_response_body(request_id).map(|body| {
StoredResponseBody {
body: body.body,
base64_encoded: body.base64_encoded,
}
})
})
}
pub fn clear_response_bodies(&mut self) {
self.response_bodies.clear();
self.response_body_order.clear();
if let Some(js) = &self.js {
js.clear_network_response_bodies();
}
}
pub fn execute_preload_script(&mut self, source: &str) -> Result<(), String> {
@@ -1463,3 +1563,17 @@ impl From<ObscuraNetError> for PageError {
PageError::NetworkError(e.to_string())
}
}
fn response_body_entry_limit() -> usize {
std::env::var("OBSCURA_NETWORK_BODY_BUFFER_ENTRIES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(128)
}
fn response_body_byte_limit() -> usize {
std::env::var("OBSCURA_NETWORK_BODY_BUFFER_BYTES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2 * 1024 * 1024)
}
+1
View File
@@ -238,6 +238,7 @@ fn is_v8_free_method(method: &str) -> bool {
| "Network.getCookies" | "Network.getAllCookies"
| "Network.setCookie" | "Network.setCookies"
| "Network.deleteCookies" | "Network.clearBrowserCookies"
| "Network.getResponseBody"
| "Fetch.continueRequest" | "Fetch.fulfillRequest"
| "Fetch.failRequest" | "Fetch.getResponseBody"
| "Storage.getCookies" | "Storage.setCookies" | "Storage.deleteCookies"
+99
View File
@@ -32,6 +32,16 @@ pub async fn handle(
) -> Result<Value, String> {
match method {
"enable" => Ok(json!({})),
"disable" => {
if let Some(page) = ctx.get_session_page_mut(session_id) {
page.clear_response_bodies();
} else {
for page in &mut ctx.pages {
page.clear_response_bodies();
}
}
Ok(json!({}))
}
"setExtraHTTPHeaders" => {
let headers = params.get("headers").and_then(|v| v.as_object());
if let Some(page) = ctx.get_session_page(session_id) {
@@ -86,6 +96,26 @@ pub async fn handle(
}
"setCacheDisabled" => Ok(json!({})),
"setRequestInterception" => Ok(json!({})),
"getResponseBody" => {
let request_id = params
.get("requestId")
.and_then(|v| v.as_str())
.ok_or("Network.getResponseBody requires requestId")?;
let body = if let Some(page) = ctx.get_session_page(session_id) {
page.get_response_body(request_id)
} else {
ctx.pages.iter().find_map(|page| page.get_response_body(request_id))
};
match body {
Some(body) => Ok(json!({
"body": body.body,
"base64Encoded": body.base64_encoded,
})),
None => Err(format!("No response body found for requestId {}", request_id)),
}
}
_ => Err(format!("Unknown Network method: {}", method)),
}
}
@@ -193,6 +223,75 @@ mod tests {
.expect("clearBrowserCookies must succeed");
assert!(ctx.default_context.cookie_jar.get_all_cookies().is_empty());
}
#[tokio::test]
async fn get_response_body_returns_stored_document_body() {
let mut ctx = CdpContext::new();
let page_id = ctx.create_page();
let session_id = Some("session-1".to_string());
ctx.sessions.insert(session_id.clone().unwrap(), page_id.clone());
let page = ctx.get_page_mut(&page_id).unwrap();
page.navigate("data:text/html,<html><body>hello body</body></html>")
.await
.unwrap();
let request_id = page.network_events[0].request_id.clone();
let result = handle(
"getResponseBody",
&json!({ "requestId": request_id }),
&mut ctx,
&session_id,
)
.await
.unwrap();
assert_eq!(result["body"], "<html><body>hello body</body></html>");
assert_eq!(result["base64Encoded"], false);
}
#[tokio::test]
async fn get_response_body_errors_for_unknown_request_id() {
let mut ctx = CdpContext::new();
let err = handle(
"getResponseBody",
&json!({ "requestId": "missing" }),
&mut ctx,
&None,
)
.await
.unwrap_err();
assert!(err.contains("missing"));
}
#[tokio::test]
async fn network_disable_clears_stored_response_bodies() {
let mut ctx = CdpContext::new();
let page_id = ctx.create_page();
let session_id = Some("session-1".to_string());
ctx.sessions.insert(session_id.clone().unwrap(), page_id.clone());
let page = ctx.get_page_mut(&page_id).unwrap();
page.navigate("data:text/html,<html><body>temporary body</body></html>")
.await
.unwrap();
let request_id = page.network_events[0].request_id.clone();
handle("disable", &json!({}), &mut ctx, &session_id)
.await
.unwrap();
let err = handle(
"getResponseBody",
&json!({ "requestId": request_id }),
&mut ctx,
&session_id,
)
.await
.unwrap_err();
assert!(err.contains("No response body found"));
}
}
pub(crate) fn cookie_info_to_cdp_json(c: &obscura_net::CookieInfo) -> Value {
+5 -5
View File
@@ -565,11 +565,11 @@ impl DomTree {
// querySelector matches strict descendants of root only, so the
// indexed element must have root among its ancestors.
Some(nid) if self.ancestors(nid).contains(&root) => return Ok(Some(nid)),
// No element has this id at all: a bare id selector cannot match.
None => return Ok(None),
// Indexed (first) element is not under a scoped root; a later
// duplicate could still be a descendant, so fall through to scan.
Some(_) => {}
// Index miss or stale entry (detached node) — fall through to full scan.
// The id_index is best-effort: it only registers nodes at creation time
// and does not update on reparent, so it can point to a detached clone
// while the live node (with the same id) is elsewhere in the tree.
_ => {}
}
}
let selector_list = parse_selector(selector)?;
+649 -124
View File
File diff suppressed because it is too large. Load diff
+82 -8
View File
@@ -1,5 +1,5 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
use std::sync::Arc;
@@ -38,6 +38,12 @@ pub struct InterceptedRequest {
pub resolver: tokio::sync::oneshot::Sender<InterceptResolution>,
}
#[derive(Debug, Clone)]
pub struct StoredNetworkResponseBody {
pub body: String,
pub base64_encoded: bool,
}
pub struct ObscuraState {
pub dom: Option<DomTree>,
pub url: String,
@@ -57,6 +63,9 @@ pub struct ObscuraState {
// `op_binding_called` op. Drained by the CDP layer after each dispatch
// and emitted as `Runtime.bindingCalled` events.
pub pending_binding_calls: Vec<(String, String)>,
pub network_response_bodies: HashMap<String, StoredNetworkResponseBody>,
pub network_response_body_order: VecDeque<String>,
pub network_response_body_counter: u64,
}
impl ObscuraState {
@@ -74,10 +83,27 @@ impl ObscuraState {
intercept_counter: 0,
intercept_enabled: false,
pending_binding_calls: Vec::new(),
network_response_bodies: HashMap::new(),
network_response_body_order: VecDeque::new(),
network_response_body_counter: 0,
}
}
}
fn response_body_entry_limit() -> usize {
std::env::var("OBSCURA_NETWORK_BODY_BUFFER_ENTRIES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(128)
}
fn response_body_byte_limit() -> usize {
std::env::var("OBSCURA_NETWORK_BODY_BUFFER_BYTES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2 * 1024 * 1024)
}
pub type SharedState = Rc<RefCell<ObscuraState>>;
#[op2]
@@ -137,7 +163,21 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
"null".into()
}
"get_element_by_id" => {
dom.get_element_by_id(&arg1).map(|id| id.index().to_string()).unwrap_or("-1".into())
// Verify the indexed node is in the live document. The id_index is best-effort:
// it only registers nodes at creation time and doesn't update on reparent, so
// it can point to a detached clone while the live node is elsewhere in the tree.
let doc = dom.document();
let nid = dom.get_element_by_id(&arg1);
let live = nid.filter(|&n| dom.ancestors(n).contains(&doc));
match live {
Some(n) => n.index().to_string(),
None => {
// Fall back to full scan for the live document.
let sel = format!("[id=\"{}\"]", arg1.replace('\\', "\\\\").replace('"', "\\\""));
dom.query_selector(&sel).ok().flatten()
.map(|id| id.index().to_string()).unwrap_or("-1".into())
}
}
}
"query_selector" => {
dom.query_selector(&arg1).ok().flatten().map(|id| id.index().to_string()).unwrap_or("-1".into())
@@ -235,19 +275,22 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
serde_json::to_string(&dom.outer_html(NodeId::new(nid))).unwrap_or("\"\"".into())
}
"append_child" => {
let parent = arg1.parse::<u32>().unwrap_or(0);
let child = arg2.parse::<u32>().unwrap_or(0);
// Reject if either nid failed to parse (was "undefined"/empty) — those
// default to 0 which is the document root, and silently operating on it
// corrupts the tree. Require both args to be valid positive integers.
let parent = match arg1.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
let child = match arg2.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
dom.append_child(NodeId::new(parent), NodeId::new(child));
"true".into()
}
"remove_child" => {
let child = arg1.parse::<u32>().unwrap_or(0);
let child = match arg1.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
dom.remove_child(NodeId::new(child));
"true".into()
}
"insert_before" => {
let new_node = arg1.parse::<u32>().unwrap_or(0);
let ref_node = arg2.parse::<u32>().unwrap_or(0);
let new_node = match arg1.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
let ref_node = match arg2.parse::<u32>() { Ok(n) => n, Err(_) => return "false".into() };
dom.insert_before(NodeId::new(ref_node), NodeId::new(new_node));
"true".into()
}
@@ -261,7 +304,12 @@ fn op_dom_inner(state: &OpState, cmd: String, arg1: String, arg2: String) -> Str
"true".into()
}
"set_inner_html" => {
let nid = arg1.parse::<u32>().unwrap_or(0);
let nid = match arg1.parse::<u32>() {
Ok(n) if n > 0 => n,
// nid=0 is the document root; never allow innerHTML to clear it.
// nid parse failure (e.g. "undefined") also falls here.
_ => return "false".into(),
};
let target = NodeId::new(nid);
let children = dom.children(target);
for child in children {
@@ -850,6 +898,31 @@ async fn op_fetch_url(
.map_err(|e| deno_error::JsErrorBox::generic(e.to_string()))?;
let resp_body = String::from_utf8_lossy(&resp_bytes).to_string();
let resp_body_base64 = BASE64.encode(&resp_bytes);
let response_request_id = {
let state_borrow = state.borrow();
let gs = state_borrow.borrow::<SharedState>().clone();
let mut gs = gs.borrow_mut();
gs.network_response_body_counter += 1;
let request_id = format!("fetch-{}", gs.network_response_body_counter);
let max_entries = response_body_entry_limit();
let max_bytes = response_body_byte_limit();
if max_entries > 0 && max_bytes > 0 && resp_bytes.len() <= max_bytes {
gs.network_response_bodies.insert(
request_id.clone(),
StoredNetworkResponseBody {
body: resp_body.clone(),
base64_encoded: false,
},
);
gs.network_response_body_order.push_back(request_id.clone());
while gs.network_response_body_order.len() > max_entries {
if let Some(oldest) = gs.network_response_body_order.pop_front() {
gs.network_response_bodies.remove(&oldest);
}
}
}
request_id
};
tracing::debug!("op_fetch_url completed: {} {} ({} bytes)", method, url, resp_body.len());
@@ -857,6 +930,7 @@ async fn op_fetch_url(
"status": status,
"body": resp_body,
"bodyBase64": resp_body_base64,
"requestId": response_request_id,
"url": url,
"headers": resp_headers,
})
+43 -2
View File
@@ -10,7 +10,7 @@ use obscura_dom::DomTree;
pub use deno_core::v8::IsolateHandle;
use crate::module_loader::ObscuraModuleLoader;
use crate::ops::{build_extension, ObscuraState};
use crate::ops::{build_extension, ObscuraState, StoredNetworkResponseBody};
static SNAPSHOT: &[u8] = include_bytes!(env!("OBSCURA_SNAPSHOT_PATH"));
@@ -130,7 +130,7 @@ impl ObscuraJsRuntime {
runtime
.execute_script(
"<obscura:init>",
"globalThis.__obscura_objects = {}; globalThis.__obscura_oid = 0; globalThis.__obscura_init();".to_string(),
"globalThis.__obscura_objects = {}; globalThis.__obscura_oid = 0;".to_string(),
)
.expect("init should not fail");
@@ -184,6 +184,16 @@ impl ObscuraJsRuntime {
std::mem::take(&mut self.state.borrow_mut().pending_binding_calls)
}
pub fn get_network_response_body(&self, request_id: &str) -> Option<StoredNetworkResponseBody> {
self.state.borrow().network_response_bodies.get(request_id).cloned()
}
pub fn clear_network_response_bodies(&self) {
let mut state = self.state.borrow_mut();
state.network_response_bodies.clear();
state.network_response_body_order.clear();
}
/// Wire up the interception channel without enabling interception.
/// Use set_intercept_enabled separately. The two were entangled before
/// and every navigation auto-enabled interception, which made
@@ -220,6 +230,22 @@ impl ObscuraJsRuntime {
);
}
pub fn set_stealth(&mut self, enabled: bool) {
let _ = self.runtime.execute_script(
"<set-stealth>",
format!("globalThis.__obscura_stealth = {};", enabled),
);
}
/// Run __obscura_init() after all per-page properties (UA, platform, stealth, etc.)
/// have been set. Must be called once per page setup, after all set_* methods.
pub fn run_page_init(&mut self) {
let _ = self.runtime.execute_script(
"<obscura:page-init>",
"globalThis.__obscura_init();".to_string(),
);
}
/// Override the coordinates the navigator.geolocation shim reports. The
/// values are injected as numeric globals the bootstrap reads; when unset it
/// keeps the built-in default. Callers validate the range before calling.
@@ -2452,6 +2478,21 @@ mod tests {
);
}
#[test]
fn console_log_error_does_not_trigger_prepare_stack_trace() {
let mut rt = setup_runtime("<div></div>");
let result = rt.evaluate(r#"
let called = false;
const saved = Error.prepareStackTrace;
Error.prepareStackTrace = function() { called = true; return saved; };
const e = new Error("test");
console.log(e);
Error.prepareStackTrace = saved;
return called;
"#).unwrap();
assert_eq!(result, serde_json::json!(false));
}
#[test]
fn element_aria_reflection_setters_write_through() {
let mut rt = setup_runtime(r#"<div id="d"></div>"#);
+3 -3
View File
@@ -23,14 +23,14 @@ tempfile = "3"
# Emulation builder reshuffled). A caret requirement let a fresh resolve pull a
# newer, incompatible rc and broke `cargo build --features stealth` (issue #234).
# Pin exact versions so the build always resolves to the API wreq_client.rs targets.
wreq-util = { version = "=3.0.0-rc.10", optional = true }
wreq-util = { version = "=3.0.0-rc.12", optional = true }
# `prefix-symbols` renames BoringSSL exports to avoid clashes with system OpenSSL,
# but the renaming is only correct on Linux and Android (per wreq's README).
# Enabling it on other platforms produces unresolved `build_script_main_*`
# symbols at link time (see issue #39).
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
wreq = { version = "=6.0.0-rc.28", optional = true, features = ["prefix-symbols", "socks"] }
wreq = { version = "=6.0.0-rc.29", optional = true, features = ["prefix-symbols", "socks"] }
[target.'cfg(not(any(target_os = "linux", target_os = "android")))'.dependencies]
wreq = { version = "=6.0.0-rc.28", optional = true, features = ["socks"] }
wreq = { version = "=6.0.0-rc.29", optional = true, features = ["socks"] }
+3 -11
View File
@@ -36,21 +36,13 @@ impl StealthHttpClient {
}
pub fn with_proxy(cookie_jar: Arc<CookieJar>, proxy_url: Option<&str>) -> Self {
// Issue #184: `set_default_paths()` reads OpenSSL's compile-time CA
// paths, which only resolve on Linux. On Windows the store ends up
// empty and every TLS handshake fails with CERTIFICATE_VERIFY_FAILED.
// `CertStore::default()` uses wreq's bundled Mozilla roots
// (`webpki-root-certs`), which works the same on every platform.
let cert_store = wreq::tls::CertStore::default();
let emulation_opts = wreq_util::EmulationOption::builder()
.emulation(wreq_util::Emulation::Chrome145)
.emulation_os(wreq_util::EmulationOS::Linux)
let emulation_opts = wreq_util::Emulation::builder()
.profile(wreq_util::Profile::Chrome145)
.platform(wreq_util::Platform::Linux)
.build();
let mut builder = wreq::Client::builder()
.emulation(emulation_opts)
.cert_store(cert_store)
.timeout(Duration::from_secs(30))
.redirect(wreq::redirect::Policy::none());
+4
View File
@@ -61,6 +61,7 @@ Run the CDP server. Puppeteer and Playwright connect over WebSocket.
--allow-file-access Permit CDP clients to navigate to file:// URLs
--storage-dir <DIR> Persistent cookies and localStorage
--allow-private-network Permit loopback / RFC1918 / link-local
-q, --quiet Suppress info logging
-v, --verbose Enable info logging
```
@@ -95,6 +96,7 @@ Run obscura as an MCP server.
```
--http HTTP transport instead of stdio
--host <HOST> HTTP bind host (default 127.0.0.1)
--port <PORT> HTTP port (default 3000)
--proxy <URL> HTTP or SOCKS5 proxy
--user-agent <UA> Override the User-Agent
@@ -103,4 +105,6 @@ Run obscura as an MCP server.
-v, --verbose Enable info logging
```
`--host` only applies with `--http`. The default `127.0.0.1` keeps the server loopback-only; set `0.0.0.0` to bind all interfaces (for example a Docker Compose sidecar) and pair it with `OBSCURA_MCP_ALLOWED_ORIGINS`.
Default transport is stdio. See [Use the MCP server](Use-the-MCP-server.md).
+25
View File
@@ -60,6 +60,31 @@ obscura serve --user-agent "Mozilla/5.0 (...) ..."
Default UA matches a recent Chrome on the build platform.
## Browser profile, timezone, and geolocation
The engine presents one of a built-in pool of realistic browser profiles (a mix of Windows and macOS, recent Chrome versions). Each profile keeps `navigator.platform`, `navigator.userAgentData` (platform and platform version), the UA string, and the WebGL/GPU renderer internally consistent, so the surfaces a site fingerprints agree with each other. Windows profiles report ANGLE Direct3D11 renderers, macOS profiles report ANGLE Metal renderers.
A single stable profile is used by default. One IP cycling through different identities is itself a signal, so rotation is opt-in:
```bash
OBSCURA_PROFILE=2 obscura serve # pin a specific profile by index
OBSCURA_ROTATE_PROFILE=1 obscura serve # random profile per browser context
```
Timezone is driven by the process zone so `Date` (`getTimezoneOffset`, `toString`) and `Intl.DateTimeFormat` report the same region. Default is `Europe/Berlin`; set it to match the exit IP:
```bash
OBSCURA_TIMEZONE=America/New_York obscura serve
```
`navigator.geolocation` reports configurable coordinates. Set them as `lat,lon` and keep them consistent with the timezone and proxy region:
```bash
OBSCURA_GEOLOCATION="40.7128,-74.0060" obscura serve
```
Keep these aligned. A rotated or mismatched profile carries no matching TLS or timezone fingerprint, so when you pin a proxy region or TLS fingerprint, leave rotation off and set the timezone and geolocation to the same region. See [Environment variables](Environment-variables.md) for the full list.
## Combine
```bash
+3 -1
View File
@@ -72,13 +72,15 @@ await page.goto('https://example.com', { waitUntil: 'load' });
- `page.goto`, `page.reload`, `page.goBack`, `page.goForward`
- `page.evaluate`, `page.evaluateHandle`
- `page.click`, `page.type`, `page.fill`
- `page.click`, `page.type`, `page.fill`, `page.focus`
- `page.waitForSelector`, `page.waitForFunction`, `page.waitForNavigation`
- `page.cookies`, `page.setCookie`, `context.cookies`
- `page.setRequestInterception`, block / modify
- `page.exposeFunction`
- `page.content`, `page.title`, `page.url`
DOM-agent frameworks such as browser-use also connect: obscura implements `DOMSnapshot.captureSnapshot` and `Target.targetInfoChanged` for perception, and `DOM.focus` so a focused field receives `Input.dispatchKeyEvent` keystrokes.
## Not supported
- `page.screenshot`: obscura doesn't render pixels.
+50 -2
View File
@@ -2,7 +2,9 @@
### `OBSCURA_ALLOW_PRIVATE_NETWORK`
Allow fetches to loopback (`127.0.0.0/8`), RFC1918 (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), and link-local (`169.254.0.0/16`) addresses. Off by default to block SSRF.
Allow fetches to loopback (`127.0.0.0/8`), RFC1918 (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), and link-local (`169.254.0.0/16`, including the `169.254.169.254` cloud-metadata endpoint) addresses. The deny-set also covers the unspecified address (`0.0.0.0` / `::`), IPv6 unique-local (`fc00::/7`), and any IPv4-mapped form of the above. Off by default to block SSRF.
The guard validates at DNS-resolution time as well as on literal hosts, so a public hostname that resolves to a forbidden address is rejected at connect time (DNS-rebinding safe), not just hosts written as raw IPs.
Truthy values: `1`, `true`, `yes`, `on`.
@@ -44,6 +46,52 @@ Default proxy URL used by `obscura-worker` for the parallel `scrape` command whe
OBSCURA_PROXY=http://proxy.example.com:8080 obscura scrape - < urls.txt
```
## Stealth and identity
These tune the browser identity the engine presents so it stays internally consistent. See [Configure stealth and proxies](Configure-stealth-and-proxies.md) for the full picture.
### `OBSCURA_TIMEZONE`
Pins the process timezone before V8/ICU reads it, so `Date` (`getTimezoneOffset`, `toString`) and `Intl.DateTimeFormat` report one consistent zone. Default `Europe/Berlin`. Set it to match the exit IP's region.
```bash
OBSCURA_TIMEZONE=America/New_York obscura serve
```
### `OBSCURA_GEOLOCATION`
Override the coordinates the `navigator.geolocation` shim reports, as `lat,lon`. Without it the shim reports a fixed default. Keep it consistent with `OBSCURA_TIMEZONE` and the proxy region.
```bash
OBSCURA_GEOLOCATION="40.7128,-74.0060" obscura serve
```
### `OBSCURA_PROFILE`
Pin a specific browser profile from the built-in pool by index (`0`-based). Each profile keeps `navigator.platform`, `userAgentData`, the UA string, and the GPU renderer internally consistent. Without it a single stable profile is used.
```bash
OBSCURA_PROFILE=2 obscura serve
```
### `OBSCURA_ROTATE_PROFILE`
Opt into picking a random profile per browser context instead of the stable default. Leave it off when you pin a TLS fingerprint, proxy region, or timezone, since a rotated profile would no longer match those.
```bash
OBSCURA_ROTATE_PROFILE=1 obscura serve
```
## MCP
### `OBSCURA_MCP_ALLOWED_ORIGINS`
Comma-separated `Origin` allowlist for the HTTP MCP transport (`obscura mcp --http`). Off by default, which keeps the permissive behavior. When set, a browser request whose `Origin` is not listed is refused with `403` before it can drive the server; native, non-browser MCP clients (which send no `Origin`) are always allowed. Use it to stop cross-origin pages from reaching a loopback MCP port.
```bash
OBSCURA_MCP_ALLOWED_ORIGINS="https://app.example.com" obscura mcp --http --host 0.0.0.0
```
## Logging
### `RUST_LOG`
@@ -76,7 +124,7 @@ V8 flags are passed via `--v8-flags`, not environment variables:
obscura serve --v8-flags "--max-old-space-size=2048 --expose-gc"
```
Default heap is 4 GB on 64-bit systems.
Defaults are `--max-old-space-size=4096 --max-semi-space-size=4 --optimize-for-size` on 64-bit systems (a 4 GB old-space ceiling, a capped young generation, and codegen tuned for a smaller footprint to cut RSS). Anything you pass with `--v8-flags` is appended after these, and V8 uses the last value for a repeated flag, so your value wins for that flag while the other defaults stay in effect.
## HTTP proxy environment
+13 -2
View File
@@ -50,13 +50,13 @@ Use one worker per CPU core. Each worker handles its own pool of pages. Sessions
## V8 heap
Default V8 heap is 4 GB on 64-bit systems. Override:
Default V8 heap is 4 GB on 64-bit systems. The defaults also cap the young generation (`--max-semi-space-size=4`) and pass `--optimize-for-size` to hold RSS down. Override:
```bash
obscura serve --v8-flags "--max-old-space-size=2048"
```
Lower for memory-constrained hosts, raise for heavy SPAs.
Flags you pass are appended after the defaults, and V8 uses the last value for a repeated flag, so your `--max-old-space-size` wins while the memory-tuning defaults stay in effect. Lower for memory-constrained hosts, raise for heavy SPAs.
## Parallel scrape
@@ -120,6 +120,17 @@ Obscura's CDP server has no built-in auth. Anyone who can reach the port can dri
Never bind `0.0.0.0` on a public IP without one of the above.
## MCP HTTP transport
`obscura mcp --http` binds `127.0.0.1` by default. To reach it from another container, bind with `--host 0.0.0.0` and set an `Origin` allowlist so a browser page cannot drive it cross-origin:
```bash
OBSCURA_MCP_ALLOWED_ORIGINS="https://app.example.com" \
obscura mcp --http --host 0.0.0.0 --port 3000
```
Request bodies are capped at 16 MiB. Like the CDP server it has no built-in auth, so keep it on an internal network or behind an authenticating proxy. See [Use the MCP server](Use-the-MCP-server.md).
## Observability
```bash
+19
View File
@@ -14,12 +14,31 @@ HTTP (for remote or shared use):
obscura mcp --http --port 3000
```
The HTTP transport binds `127.0.0.1` by default. Bind all interfaces with `--host` for a container or sidecar deployment:
```bash
obscura mcp --http --host 0.0.0.0 --port 3000
```
With stealth and proxy:
```bash
obscura mcp --stealth --proxy http://proxy.example.com:8080
```
## Security
The HTTP transport has no built-in auth, so anyone who can reach the port can drive the browser. Two guards ship for the HTTP transport:
- **Origin allowlist.** Set `OBSCURA_MCP_ALLOWED_ORIGINS` to a comma-separated list of allowed `Origin` values. When set, a browser request from an unlisted origin is refused with `403` before it can drive the server, which blocks a malicious page from POSTing to a loopback MCP port. Native, non-browser clients send no `Origin` and are always allowed. Unset (the default) keeps the permissive behavior.
- **Body cap.** A single request body is capped at 16 MiB, so an unauthenticated caller cannot force a large allocation with an oversized `Content-Length`.
```bash
OBSCURA_MCP_ALLOWED_ORIGINS="https://app.example.com" obscura mcp --http --host 0.0.0.0
```
When you expose the HTTP transport beyond loopback, set the allowlist and put it behind a reverse proxy or network isolation that enforces auth.
## Tools exposed
The server keeps a live browser session, so tools operate on the current page rather than taking a URL each call. Navigate first, then read or act.