From 2e731a2ab97cc3086caec683eb1473e9cea15250 Mon Sep 17 00:00:00 2001 From: zhangyang-crazy-one <740884666@qq.com> Date: Fri, 5 Jun 2026 02:20:51 +0800 Subject: [PATCH] feat(api): add obscura-api crate with Browser + Page + Element + CookieStore (#191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(api): add obscura-api crate with Browser + Page API Adds a Rust library crate exposing Obscura as a programmable browser: - Browser: build with config (proxy, stealth, UA, storage_dir) - Page: goto, url, evaluate, content - BrowserBuilder with fluent API - examples/basic.rs Part of #169 * feat(api): add Element API + CookieStore + wait_for_selector - Element: text(), attribute(), click() via JS evaluation - CookieStore: set/get_all/get_for_url/save_to_file/load_from_file - Page::wait_for_selector with 100ms polling - Page::query_selector for single element lookup - Updated examples/basic.rs with full interaction demo Part of #169 * feat(api): add Browser::cookies() method * refactor(api): move to obscura crate with feature flag - Renamed obscura-api → obscura (per maintainer feedback #169) - API enabled by default via 'api' feature flag - Usage: cargo add obscura (default=api) or cargo add obscura --features api - Removed separate obscura-api crate * chore: exclude docker workflow from fork * fix(api): make the obscura crate build, run, and document install correctly - examples/basic.rs and the lib.rs doctest imported `obscura_api`, but the crate is named `obscura`, so both failed to compile. Use the real crate name. - wait_for_selector / query_selector read the node id with `Value::as_u64`, but `evaluate` returns JS numbers as f64, so it was always None and every wait_for_selector timed out. Accept an integer or a non-negative finite float. - restore .github/workflows/docker.yml, which the PR removed (a fork-specific change that should not land upstream). - add crates/obscura/README.md with honest install instructions: the crate is not on crates.io, so depend on it via git (and it builds the embedded V8 from source). The `cargo add obscura` form does not work yet. Verified: workspace + example build, the lib doctest passes, the example runs end to end (goto, wait_for_selector, text, attribute), and the integration suite is 28/28 with no regressions. --------- Co-authored-by: zhangyang-crazy-one Co-authored-by: SGavrl <277843151+SGavrl@users.noreply.github.com> --- Cargo.lock | 375 ++++++++++++++++---------- Cargo.toml | 1 + crates/obscura-cdp/src/domains/dom.rs | 10 +- crates/obscura/Cargo.toml | 22 ++ crates/obscura/README.md | 43 +++ crates/obscura/examples/basic.rs | 22 ++ crates/obscura/src/browser.rs | 104 +++++++ crates/obscura/src/config.rs | 61 +++++ crates/obscura/src/cookie.rs | 96 +++++++ crates/obscura/src/error.rs | 22 ++ crates/obscura/src/lib.rs | 28 ++ crates/obscura/src/page.rs | 137 ++++++++++ 12 files changed, 781 insertions(+), 140 deletions(-) create mode 100644 crates/obscura/Cargo.toml create mode 100644 crates/obscura/README.md create mode 100644 crates/obscura/examples/basic.rs create mode 100644 crates/obscura/src/browser.rs create mode 100644 crates/obscura/src/config.rs create mode 100644 crates/obscura/src/cookie.rs create mode 100644 crates/obscura/src/error.rs create mode 100644 crates/obscura/src/lib.rs create mode 100644 crates/obscura/src/page.rs diff --git a/Cargo.lock b/Cargo.lock index aa8fccf..eb65679 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,9 +102,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-compression" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" dependencies = [ "compression-codecs", "compression-core", @@ -131,9 +131,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "az" @@ -182,7 +182,7 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn", ] @@ -200,7 +200,7 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn", ] @@ -221,9 +221,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "bitvec" @@ -273,9 +273,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -294,9 +294,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -332,14 +332,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.59" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -376,9 +376,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -398,9 +398,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -431,9 +431,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "compression-codecs" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" dependencies = [ "brotli", "compression-core", @@ -443,9 +443,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] name = "cooked-waker" @@ -535,9 +535,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "debugid" @@ -691,9 +691,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -726,9 +726,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encoding_rs" @@ -1012,9 +1012,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "gzip-header" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +checksum = "86848f4fd157d91041a62c78046fb7b248bcc2dce78376d436a1756e9a038577" dependencies = [ "crc32fast", ] @@ -1036,9 +1036,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1069,9 +1069,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1102,9 +1102,9 @@ dependencies = [ [[package]] name = "http2" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7349aa548b6a413a7e7146d5d7e3db4565b3d0fbc8ff53c72e46b44b083cf9f" +checksum = "569ef7a780e853c4e1768f58a3c8168193b82cdcbab66638a0b1c6583ec5995e" dependencies = [ "atomic-waker", "bytes", @@ -1128,9 +1128,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1286,9 +1286,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1302,12 +1302,12 @@ checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1318,16 +1318,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1361,9 +1351,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -1385,9 +1375,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -1434,9 +1424,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "lru-slab" @@ -1486,9 +1476,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "minimal-lexical" @@ -1508,9 +1498,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1550,14 +1540,14 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -1577,6 +1567,20 @@ dependencies = [ "autocfg", ] +[[package]] +name = "obscura" +version = "0.1.0" +dependencies = [ + "anyhow", + "obscura-browser", + "obscura-net", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "url", +] + [[package]] name = "obscura-browser" version = "0.1.0" @@ -1794,7 +1798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -1821,18 +1825,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -1847,9 +1851,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "potential_utf" @@ -1968,7 +1972,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -1991,7 +1995,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2023,18 +2027,18 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -2173,7 +2177,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2205,9 +2209,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -2310,9 +2314,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap", "itoa", @@ -2394,6 +2398,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -2412,9 +2422,9 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -2430,9 +2440,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2565,9 +2575,9 @@ dependencies = [ [[package]] name = "sys_traits" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a79feaa49de4a6c8191bdbd5fb3eada50671e9367d874d1c12e3d36db131414" +checksum = "88826d6169418c98e8bc52a43f79d50907dfa3e87ba5161930d42c6f980b35ee" dependencies = [ "sys_traits_macros", ] @@ -2720,9 +2730,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -2820,9 +2830,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", "bitflags", @@ -2832,13 +2842,13 @@ dependencies = [ "http", "http-body", "http-body-util", - "iri-string", "pin-project-lite", "tokio", "tokio-util", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2931,7 +2941,7 @@ dependencies = [ "http", "httparse", "log", - "rand 0.9.2", + "rand 0.9.4", "sha1", "thiserror 2.0.18", "utf-8", @@ -2959,9 +2969,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-id-start" @@ -3020,9 +3030,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -3080,11 +3090,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -3093,14 +3103,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3111,9 +3121,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -3121,9 +3131,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3131,9 +3141,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -3144,9 +3154,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -3197,9 +3207,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -3217,9 +3227,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -3279,7 +3289,25 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -3297,14 +3325,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -3313,48 +3358,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winsafe" version = "0.0.19" @@ -3370,6 +3463,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -3511,9 +3610,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3534,18 +3633,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", @@ -3554,9 +3653,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] diff --git a/Cargo.toml b/Cargo.toml index 345618a..4bc66eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "crates/obscura-js", "crates/obscura-mcp", "crates/obscura-cli", + "crates/obscura", ] [workspace.package] diff --git a/crates/obscura-cdp/src/domains/dom.rs b/crates/obscura-cdp/src/domains/dom.rs index a61bf8d..fb2f4aa 100644 --- a/crates/obscura-cdp/src/domains/dom.rs +++ b/crates/obscura-cdp/src/domains/dom.rs @@ -167,7 +167,10 @@ pub async fn handle( "removeNode" => Ok(json!({})), "getBoxModel" => { let page = ctx.get_session_page_mut(session_id).ok_or("No page")?; - let node_id = resolve_node_id(page, params)?; + let node_id = match resolve_node_id(page, params) { + Ok(nid) => nid, + Err(_) => return Ok(json!(null)), + }; let code = format!( "(function() {{\ var el = globalThis._wrap && globalThis._wrap({0});\ @@ -202,7 +205,10 @@ pub async fn handle( } "getContentQuads" => { let page = ctx.get_session_page_mut(session_id).ok_or("No page")?; - let node_id = resolve_node_id(page, params)?; + let node_id = match resolve_node_id(page, params) { + Ok(nid) => nid, + Err(_) => return Ok(json!(null)), + }; let code = format!( "(function() {{\ var el = globalThis._wrap && globalThis._wrap({0});\ diff --git a/crates/obscura/Cargo.toml b/crates/obscura/Cargo.toml new file mode 100644 index 0000000..2d7edac --- /dev/null +++ b/crates/obscura/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "obscura" +version = "0.1.0" +edition = "2021" +description = "Rust API for the Obscura headless browser" +readme = "README.md" +repository = "https://github.com/h4ckf0r0day/obscura" +license = "Apache-2.0" + +[features] +default = ["api"] +api = ["obscura-browser", "obscura-net", "tokio"] + +[dependencies] +obscura-browser = { path = "../obscura-browser", optional = true } +obscura-net = { path = "../obscura-net", optional = true } +anyhow = "1" +tokio = { version = "1", features = ["rt"], optional = true } +thiserror = "1" +serde_json = "1" +serde = { version = "1", features = ["derive"] } +url = "2" diff --git a/crates/obscura/README.md b/crates/obscura/README.md new file mode 100644 index 0000000..c2d1a55 --- /dev/null +++ b/crates/obscura/README.md @@ -0,0 +1,43 @@ +# obscura + +Embeddable Rust API for the [Obscura](https://github.com/h4ckf0r0day/obscura) +headless browser. Drive a real V8 + DOM browser (`Browser`, `Page`, `Element`, +`CookieStore`) directly from Rust, with no separate process or CDP round-trips. + +## Install + +This crate is not published to crates.io, so depend on it via git. Building it +compiles Obscura from source, including its embedded V8 (`deno_core`), so the +first build is large and slow. + +```toml +[dependencies] +obscura = { git = "https://github.com/h4ckf0r0day/obscura", features = ["api"] } +tokio = { version = "1", features = ["rt", "macros"] } +anyhow = "1" +``` + +## Usage + +```rust,no_run +use obscura::Browser; +use std::time::Duration; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let browser = Browser::builder() + .stealth(true) + .storage_dir("/tmp/cookies") + .build()?; + + let mut page = browser.new_page().await?; + page.goto("https://example.com").await?; + + let el = page.wait_for_selector("a", Duration::from_secs(5)).await?; + println!("{} -> {:?}", el.text(), el.attribute("href")); + + Ok(()) +} +``` + +See `examples/basic.rs` for a runnable version (`cargo run --example basic`). diff --git a/crates/obscura/examples/basic.rs b/crates/obscura/examples/basic.rs new file mode 100644 index 0000000..c4be26b --- /dev/null +++ b/crates/obscura/examples/basic.rs @@ -0,0 +1,22 @@ +/// Full example: launch, navigate, interact, check cookies. +use obscura::Browser; +use std::time::Duration; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let browser = Browser::builder() + .stealth(true) + .storage_dir("/tmp/obscura-api-test") + .build()?; + + let mut page = browser.new_page().await?; + page.goto("https://example.com").await?; + println!("URL: {}", page.url()); + println!("Content length: {}", page.content().len()); + + let el = page.wait_for_selector("a", Duration::from_secs(5)).await?; + println!("Link text: {}", el.text()); + println!("Link href: {:?}", el.attribute("href")); + + Ok(()) +} diff --git a/crates/obscura/src/browser.rs b/crates/obscura/src/browser.rs new file mode 100644 index 0000000..d7193a1 --- /dev/null +++ b/crates/obscura/src/browser.rs @@ -0,0 +1,104 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; + +use obscura_browser::BrowserContext; +use obscura_net::CookieJar; + +use crate::config::BrowserConfig; +use crate::cookie::CookieStore; +use crate::error::Error; +use crate::page::Page; + +static NEXT_PAGE_ID: AtomicU64 = AtomicU64::new(1); + +pub struct Browser { + context: Arc, + cookie_jar: Arc, +} + +impl Browser { + pub fn new() -> Result { + Self::build(BrowserConfig::default()) + } + + pub fn build(config: BrowserConfig) -> Result { + let cookie_jar = Arc::new(CookieJar::new()); + + if let Some(ref dir) = config.storage_dir { + let cookie_path = dir.join("cookies.json"); + if cookie_path.exists() { + let _ = cookie_jar.load_from_file(&cookie_path); + } + } + + let context = if let Some(ref dir) = config.storage_dir { + BrowserContext::with_storage_full( + "api".to_string(), + config.proxy, + config.stealth, + config.user_agent, + Some(dir.clone()), + ) + } else { + BrowserContext::with_full_options( + "api".to_string(), + config.proxy, + config.stealth, + config.user_agent, + ) + }; + + Ok(Browser { + context: Arc::new(context), + cookie_jar, + }) + } + + pub fn builder() -> BrowserBuilder { + BrowserBuilder::default() + } + + pub async fn new_page(&self) -> Result { + let id = NEXT_PAGE_ID.fetch_add(1, Ordering::Relaxed); + let page = obscura_browser::Page::new( + format!("page-{}", id), + self.context.clone(), + ); + Ok(Page { + inner: page, + context: self.context.clone(), + }) + } + + /// Access the cookie store for this browser session. + pub fn cookies(&self) -> CookieStore { + CookieStore::new(self.cookie_jar.clone()) + } +} + +#[derive(Default)] +pub struct BrowserBuilder { + config: BrowserConfig, +} + +impl BrowserBuilder { + pub fn proxy(mut self, proxy: impl Into) -> Self { + self.config.proxy = Some(proxy.into()); + self + } + pub fn stealth(mut self, stealth: bool) -> Self { + self.config.stealth = stealth; + self + } + pub fn user_agent(mut self, ua: impl Into) -> Self { + self.config.user_agent = Some(ua.into()); + self + } + pub fn storage_dir(mut self, dir: impl Into) -> Self { + self.config.storage_dir = Some(dir.into()); + self + } + pub fn build(self) -> Result { + Browser::build(self.config) + } +} diff --git a/crates/obscura/src/config.rs b/crates/obscura/src/config.rs new file mode 100644 index 0000000..c41fe75 --- /dev/null +++ b/crates/obscura/src/config.rs @@ -0,0 +1,61 @@ +use std::path::PathBuf; + +/// Configuration for launching a Browser instance. +pub struct BrowserConfig { + /// Proxy URL (e.g., "socks5://127.0.0.1:1080") + pub proxy: Option, + /// Enable stealth mode (fingerprint spoofing) + pub stealth: bool, + /// Custom User-Agent string + pub user_agent: Option, + /// Directory for persistent cookie storage + pub storage_dir: Option, +} + +impl Default for BrowserConfig { + fn default() -> Self { + Self { + proxy: None, + stealth: false, + user_agent: None, + storage_dir: None, + } + } +} + +impl BrowserConfig { + pub fn builder() -> BrowserConfigBuilder { + BrowserConfigBuilder::default() + } +} + +#[derive(Default)] +pub struct BrowserConfigBuilder { + config: BrowserConfig, +} + +impl BrowserConfigBuilder { + pub fn proxy(mut self, proxy: impl Into) -> Self { + self.config.proxy = Some(proxy.into()); + self + } + + pub fn stealth(mut self, stealth: bool) -> Self { + self.config.stealth = stealth; + self + } + + pub fn user_agent(mut self, ua: impl Into) -> Self { + self.config.user_agent = Some(ua.into()); + self + } + + pub fn storage_dir(mut self, dir: impl Into) -> Self { + self.config.storage_dir = Some(dir.into()); + self + } + + pub fn build(self) -> BrowserConfig { + self.config + } +} diff --git a/crates/obscura/src/cookie.rs b/crates/obscura/src/cookie.rs new file mode 100644 index 0000000..04c65a4 --- /dev/null +++ b/crates/obscura/src/cookie.rs @@ -0,0 +1,96 @@ +use std::sync::Arc; +use obscura_net::CookieJar; +use serde::{Deserialize, Serialize}; + +/// A cookie as exposed to the Rust API. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Cookie { + pub name: String, + pub value: String, + pub domain: String, + pub path: String, + pub secure: bool, + pub http_only: bool, +} + +impl Cookie { + /// Create a cookie from name=value pair with defaults. + pub fn new(name: impl Into, value: impl Into, domain: impl Into) -> Self { + Self { + name: name.into(), + value: value.into(), + domain: domain.into(), + path: "/".into(), + secure: false, + http_only: false, + } + } +} + +/// Cookie management for a browser session. +pub struct CookieStore { + jar: Arc, +} + +impl CookieStore { + pub(crate) fn new(jar: Arc) -> Self { + Self { jar } + } + + /// Set a cookie via Set-Cookie header string. + /// + /// Example: `store.set("session=abc123; Domain=example.com; Path=/; HttpOnly")?;` + pub fn set(&self, set_cookie_str: &str, url: &str) -> Result<(), crate::error::Error> { + let parsed = url::Url::parse(url) + .map_err(|e| crate::error::Error::Internal(e.into()))?; + self.jar.set_cookie(set_cookie_str, &parsed); + Ok(()) + } + + /// Get all cookies as a serializable list. + pub fn get_all(&self) -> Vec { + self.jar.get_all_cookies() + .into_iter() + .map(|c| Cookie { + name: c.name, + value: c.value, + domain: c.domain, + path: c.path, + secure: c.secure, + http_only: c.http_only, + }) + .collect() + } + + /// Get cookies for a specific URL. + pub fn get_for_url(&self, url: &str) -> Result, crate::error::Error> { + let parsed = url::Url::parse(url) + .map_err(|e| crate::error::Error::Internal(e.into()))?; + let header = self.jar.get_cookie_header(&parsed); + Ok(header + .split("; ") + .filter(|s| !s.is_empty()) + .filter_map(|pair| { + let mut parts = pair.splitn(2, '='); + Some(Cookie { + name: parts.next()?.to_string(), + value: parts.next().unwrap_or("").to_string(), + domain: parsed.host_str()?.to_string(), + path: "/".into(), + secure: false, + http_only: false, + }) + }) + .collect()) + } + + /// Save cookies to a file (JSON format). + pub fn save_to_file(&self, path: &std::path::Path) -> Result<(), crate::error::Error> { + self.jar.save_to_file(path).map_err(|e| crate::error::Error::Internal(e.into())) + } + + /// Load cookies from a file. + pub fn load_from_file(&self, path: &std::path::Path) -> Result { + self.jar.load_from_file(path).map_err(|e| crate::error::Error::Internal(e.into())) + } +} diff --git a/crates/obscura/src/error.rs b/crates/obscura/src/error.rs new file mode 100644 index 0000000..a30487a --- /dev/null +++ b/crates/obscura/src/error.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("navigation error: {0}")] + Navigation(String), + + #[error("JS evaluation error: {0}")] + JsEval(String), + + #[error("timeout: {0}")] + Timeout(String), + + #[error("element not found: {0}")] + ElementNotFound(String), + + #[error("no page session")] + NoPage, + + #[error(transparent)] + Internal(#[from] anyhow::Error), +} diff --git a/crates/obscura/src/lib.rs b/crates/obscura/src/lib.rs new file mode 100644 index 0000000..df4c44d --- /dev/null +++ b/crates/obscura/src/lib.rs @@ -0,0 +1,28 @@ +//! Rust API for the Obscura headless browser. +//! +//! ```rust,no_run +//! use obscura::Browser; +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! let browser = Browser::builder() +//! .stealth(true) +//! .build()?; +//! let mut page = browser.new_page().await?; +//! page.goto("https://example.com").await?; +//! println!("Content: {} bytes", page.content().len()); +//! Ok(()) +//! } +//! ``` + +mod browser; +mod config; +mod cookie; +mod error; +mod page; + +pub use browser::Browser; +pub use config::BrowserConfig; +pub use cookie::{Cookie, CookieStore}; +pub use error::Error; +pub use page::Page; diff --git a/crates/obscura/src/page.rs b/crates/obscura/src/page.rs new file mode 100644 index 0000000..0bdad3a --- /dev/null +++ b/crates/obscura/src/page.rs @@ -0,0 +1,137 @@ +use std::sync::Arc; +use std::time::Duration; + +use obscura_browser::lifecycle::WaitUntil; +use obscura_browser::{BrowserContext, Page as InnerPage}; +use serde_json::Value; + +use crate::error::Error; + +/// Read a DOM node id from a JS `evaluate` result. obscura serializes JS numbers +/// as f64, so `Value::as_u64` returns None for an integer-valued result; accept +/// either an integer or a non-negative finite float. null / non-numbers -> None. +fn nid_from_value(v: &Value) -> Option { + v.as_u64() + .or_else(|| v.as_f64().filter(|f| f.is_finite() && *f >= 0.0).map(|f| f as u64)) +} + +/// A browser tab/page. +pub struct Page { + pub(crate) inner: InnerPage, + pub(crate) context: Arc, +} + +impl Page { + /// Navigate to URL and wait for load. + pub async fn goto(&mut self, url: &str) -> Result<(), Error> { + self.inner + .navigate_with_wait(url, WaitUntil::Load) + .await + .map_err(|e| Error::Navigation(e.to_string())) + } + + /// Get current URL. + pub fn url(&self) -> String { + self.inner.url_string() + } + + /// Execute JS in the page. + pub fn evaluate(&mut self, expression: &str) -> Value { + self.inner.evaluate(expression) + } + + /// Get page HTML content. + pub fn content(&mut self) -> String { + let val = self.evaluate("document.documentElement.outerHTML"); + val.as_str().unwrap_or("").to_string() + } + + /// Query a single element by CSS selector. + pub fn query_selector(&mut self, selector: &str) -> Option { + let escaped = selector.replace('\\', "\\\\").replace('\'', "\\'"); + let js = format!( + "(function() {{ var el = document.querySelector('{}'); return el ? el._nid : null; }})()", + escaped + ); + let val = self.evaluate(&js); + nid_from_value(&val).map(|nid| Element { node_id: nid, page: self as *const Page }) + } + + /// Wait for CSS selector to appear (polls every 100ms). + pub async fn wait_for_selector( + &mut self, + selector: &str, + timeout: Duration, + ) -> Result { + let start = std::time::Instant::now(); + let escaped = selector.replace('\\', "\\\\").replace('\'', "\\'"); + loop { + let js = format!( + "(function() {{ var el = document.querySelector('{}'); return el ? el._nid : null; }})()", + escaped + ); + let val = self.evaluate(&js); + if let Some(nid) = nid_from_value(&val) { + return Ok(Element { node_id: nid, page: self as *const Page }); + } + if start.elapsed() > timeout { + return Err(Error::Timeout(format!( + "wait_for_selector({}) timed out after {}ms", + selector, + timeout.as_millis() + ))); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + } +} + +/// Handle to a DOM element. +/// +/// Created via [`Page::query_selector`] or [`Page::wait_for_selector`]. +pub struct Element { + node_id: u64, + page: *const Page, +} + +impl Element { + /// Get text content of this element. + pub fn text(&self) -> String { + let page = unsafe { &mut *(self.page as *mut Page) }; + let val = page.evaluate(&format!( + "(function() {{ var el = globalThis._wrap && globalThis._wrap({}); return el ? el.textContent : ''; }})()", + self.node_id + )); + val.as_str().unwrap_or("").to_string() + } + + /// Get an attribute value. + pub fn attribute(&self, name: &str) -> Option { + let page = unsafe { &mut *(self.page as *mut Page) }; + let val = page.evaluate(&format!( + "(function() {{ var el = globalThis._wrap && globalThis._wrap({}); return el ? el.getAttribute('{}') : null; }})()", + self.node_id, name + )); + if val.is_null() { None } else { Some(val.as_str().unwrap_or("").to_string()) } + } + + /// Click this element. + pub fn click(&self) -> Result<(), Error> { + let page = unsafe { &mut *(self.page as *mut Page) }; + // Scroll into view + page.evaluate(&format!( + "(function() {{ var el = globalThis._wrap && globalThis._wrap({}); if (el) el.scrollIntoView({{block:'center'}}); }})()", + self.node_id + )); + // Click + let result = page.evaluate(&format!( + "(function() {{ var el = globalThis._wrap && globalThis._wrap({}); if (el) {{ el.click(); return true; }} return false; }})()", + self.node_id + )); + if result.as_bool().unwrap_or(false) { + Ok(()) + } else { + Err(Error::ElementNotFound("click failed".into())) + } + } +}