Fix/rescue 227 regressions (#232)
* fix: restore regressions introduced by #227 #227 deleted build.rs (reverting #224's version-from-tag feature), removed DOM cycle guards in append_child/insert_before that prevent infinite loops on self-append/self-insert, dropped the CharacterData textContent fast-path, removed PI/Doctype Rust ops, and cut the env_allows_private_network() check from validate_fetch_url. Restored: - crates/obscura-cli/build.rs (version from tag/env) - OBSCURA_BUILD_VERSION in version string and banner - allow_private_network env var propagation in main.rs - append_child/insert_before self-cycle guards in tree.rs - textContent CharacterData fast-path in tree.rs - create_processing_instruction, create_doctype, pi_target, doctype_name, doctype_public_id ops in ops.rs - ProcessingInstruction branch in set_text_content op - env_allows_private_network() check in validate_fetch_url * fix(js): Europe/Berlin timezone, plugins iterator recursion Two bugs in the stealth bootstrap code from #227: - 'Europe/Germany' is not a valid IANA timezone; Intl.DateTimeFormat silently fell back to UTC. Changed to 'Europe/Berlin'. - plugins[Symbol.iterator] called this[Symbol.iterator]() recursively, blowing the stack on Array.from(navigator.plugins). Fixed to bind Array.prototype[Symbol.iterator] directly to the array.
This commit is contained in:
1 parent
990f2a63c8
commit
6b3cc1a686
5 files changed
+130
-6
No files matched your search
@@ -0,0 +1,36 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
fn env_value(name: &str) -> Option<String> {
|
||||||
|
env::var(name)
|
||||||
|
.ok()
|
||||||
|
.map(|value| value.trim().to_string())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_version(version: String) -> String {
|
||||||
|
version.strip_prefix('v').unwrap_or(&version).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn github_tag_version() -> Option<String> {
|
||||||
|
if env_value("GITHUB_REF_TYPE").as_deref() == Some("tag") {
|
||||||
|
return env_value("GITHUB_REF_NAME").map(normalize_version);
|
||||||
|
}
|
||||||
|
|
||||||
|
env_value("GITHUB_REF")
|
||||||
|
.and_then(|value| value.strip_prefix("refs/tags/").map(str::to_owned))
|
||||||
|
.map(normalize_version)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-env-changed=OBSCURA_VERSION");
|
||||||
|
println!("cargo:rerun-if-env-changed=GITHUB_REF_TYPE");
|
||||||
|
println!("cargo:rerun-if-env-changed=GITHUB_REF_NAME");
|
||||||
|
println!("cargo:rerun-if-env-changed=GITHUB_REF");
|
||||||
|
|
||||||
|
let version = env_value("OBSCURA_VERSION")
|
||||||
|
.map(normalize_version)
|
||||||
|
.or_else(github_tag_version)
|
||||||
|
.unwrap_or_else(|| env::var("CARGO_PKG_VERSION").expect("Cargo sets CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
|
println!("cargo:rustc-env=OBSCURA_BUILD_VERSION={version}");
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ use tokio::time::{timeout, Duration};
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "obscura",
|
name = "obscura",
|
||||||
version = env!("CARGO_PKG_VERSION"),
|
version = env!("OBSCURA_BUILD_VERSION"),
|
||||||
about = "Obscura - A lightweight headless browser for web scraping and automation",
|
about = "Obscura - A lightweight headless browser for web scraping and automation",
|
||||||
)]
|
)]
|
||||||
struct Args {
|
struct Args {
|
||||||
@@ -189,9 +189,9 @@ fn print_banner(port: u16) {
|
|||||||
| |__| | |_) \__ \ (__| |_| | | | (_| |
|
| |__| | |_) \__ \ (__| |_| | | | (_| |
|
||||||
\____/|_.__/|___/\___|\__,_|_| \__,_|
|
\____/|_.__/|___/\___|\__,_|_| \__,_|
|
||||||
|
|
||||||
Headless Browser v0.1.6
|
Headless Browser v{}
|
||||||
CDP server: ws://127.0.0.1:{}/devtools/browser
|
CDP server: ws://127.0.0.1:{}/devtools/browser
|
||||||
"#, port);
|
"#, env!("OBSCURA_BUILD_VERSION"), port);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_log_filter(verbose: bool, quiet: bool) -> &'static str {
|
fn select_log_filter(verbose: bool, quiet: bool) -> &'static str {
|
||||||
@@ -263,6 +263,17 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
tracing::debug!("V8 flags: {}", v8_flags);
|
tracing::debug!("V8 flags: {}", v8_flags);
|
||||||
obscura_js::set_v8_flags(&v8_flags);
|
obscura_js::set_v8_flags(&v8_flags);
|
||||||
|
|
||||||
|
// The js-side fetch path (op_fetch_url) reads OBSCURA_ALLOW_PRIVATE_NETWORK
|
||||||
|
// directly for its SSRF gate. Mirror the CLI flag into the env var so
|
||||||
|
// iframe loads and JS fetch() see the same policy the http_client layer
|
||||||
|
// already uses (issue #33).
|
||||||
|
if args.allow_private_network {
|
||||||
|
// SAFETY: set_var is unsafe in newer rustc; this runs before any
|
||||||
|
// spawned thread inspects the env, so it's effectively single
|
||||||
|
// threaded at this point.
|
||||||
|
unsafe { std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1"); }
|
||||||
|
}
|
||||||
|
|
||||||
let global_proxy = args.proxy.clone();
|
let global_proxy = args.proxy.clone();
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
|
|||||||
@@ -223,6 +223,14 @@ impl DomTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn append_child(&self, parent_id: NodeId, child_id: NodeId) {
|
pub fn append_child(&self, parent_id: NodeId, child_id: NodeId) {
|
||||||
|
// Per DOM spec, appending a node to itself is a HierarchyRequestError;
|
||||||
|
// here we treat it as a no-op rather than panic. Without this the
|
||||||
|
// sibling-pointer fixup below sets the node's prev_sibling to itself
|
||||||
|
// and every later child-walk loops forever (same failure mode that
|
||||||
|
// insert_before's self-cycle guard was added to prevent).
|
||||||
|
if parent_id == child_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.detach(child_id);
|
self.detach(child_id);
|
||||||
|
|
||||||
let mut inner = self.inner.borrow_mut();
|
let mut inner = self.inner.borrow_mut();
|
||||||
@@ -252,6 +260,15 @@ impl DomTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_before(&self, existing_id: NodeId, new_sibling_id: NodeId) {
|
pub fn insert_before(&self, existing_id: NodeId, new_sibling_id: NodeId) {
|
||||||
|
// Per DOM spec: if the node being inserted IS the reference node,
|
||||||
|
// the operation is a no-op (the node is already in its target
|
||||||
|
// position). Without this, the linked-list fixup below sets the
|
||||||
|
// node's prev_sibling and next_sibling to itself, creating a cycle
|
||||||
|
// -- every later traversal (childNodes, querySelectorAll, etc) then
|
||||||
|
// loops forever and the test page hangs while obscura burns RAM.
|
||||||
|
if existing_id == new_sibling_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let (parent_id, prev_id) = {
|
let (parent_id, prev_id) = {
|
||||||
let inner = self.inner.borrow();
|
let inner = self.inner.borrow();
|
||||||
let node = match inner.nodes.get(existing_id.index()).and_then(|n| n.as_ref()) {
|
let node = match inner.nodes.get(existing_id.index()).and_then(|n| n.as_ref()) {
|
||||||
@@ -463,6 +480,20 @@ impl DomTree {
|
|||||||
|
|
||||||
pub fn text_content(&self, node_id: NodeId) -> String {
|
pub fn text_content(&self, node_id: NodeId) -> String {
|
||||||
let inner = self.inner.borrow();
|
let inner = self.inner.borrow();
|
||||||
|
// Per DOM spec, calling textContent ON a CharacterData node
|
||||||
|
// (Text, Comment, ProcessingInstruction) returns its .data.
|
||||||
|
// Calling textContent on an Element walks descendants and
|
||||||
|
// concatenates Text node content only (Comment + PI are
|
||||||
|
// skipped). Handle the direct-CharacterData case here so the
|
||||||
|
// descent helper can keep its element-centric behavior.
|
||||||
|
if let Some(Some(node)) = inner.nodes.get(node_id.index()) {
|
||||||
|
match &node.data {
|
||||||
|
NodeData::Text { contents } => return contents.clone(),
|
||||||
|
NodeData::Comment { contents } => return contents.clone(),
|
||||||
|
NodeData::ProcessingInstruction { data, .. } => return data.clone(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
collect_text_inner(&inner, node_id, &mut result);
|
collect_text_inner(&inner, node_id, &mut result);
|
||||||
result
|
result
|
||||||
@@ -570,6 +601,10 @@ fn collect_text_inner(inner: &DomTreeInner, node_id: NodeId, buf: &mut String) {
|
|||||||
if let Some(Some(node)) = inner.nodes.get(node_id.index()) {
|
if let Some(Some(node)) = inner.nodes.get(node_id.index()) {
|
||||||
match &node.data {
|
match &node.data {
|
||||||
NodeData::Text { contents } => buf.push_str(contents),
|
NodeData::Text { contents } => buf.push_str(contents),
|
||||||
|
// Comment and ProcessingInstruction are intentionally NOT
|
||||||
|
// appended when traversing descendants: per spec, textContent
|
||||||
|
// on an Element only includes Text descendants. Direct
|
||||||
|
// textContent on a Comment/PI is handled by the caller.
|
||||||
_ => {
|
_ => {
|
||||||
let mut child = node.first_child;
|
let mut child = node.first_child;
|
||||||
while let Some(child_id) = child {
|
while let Some(child_id) = child {
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1842,7 +1842,7 @@ globalThis.navigator = {
|
|||||||
p.item = (i) => p[i] || null;
|
p.item = (i) => p[i] || null;
|
||||||
p.namedItem = (name) => p.find(x => x.name === name) || null;
|
p.namedItem = (name) => p.find(x => x.name === name) || null;
|
||||||
p.refresh = () => {};
|
p.refresh = () => {};
|
||||||
p[Symbol.iterator] = function() { return this[Symbol.iterator](); };
|
p[Symbol.iterator] = Array.prototype[Symbol.iterator].bind(p);
|
||||||
return p;
|
return p;
|
||||||
},
|
},
|
||||||
get mimeTypes() {
|
get mimeTypes() {
|
||||||
@@ -4312,7 +4312,7 @@ _markNative(MediaStream); _markNative(MediaStreamTrack);
|
|||||||
_markNative(RTCPeerConnection); _markNative(RTCSessionDescription); _markNative(RTCIceCandidate);
|
_markNative(RTCPeerConnection); _markNative(RTCSessionDescription); _markNative(RTCIceCandidate);
|
||||||
|
|
||||||
const _OrigDateTimeFormat = Intl.DateTimeFormat;
|
const _OrigDateTimeFormat = Intl.DateTimeFormat;
|
||||||
const _defaultTZ = 'Europe/Germany';
|
const _defaultTZ = 'Europe/Berlin';
|
||||||
Intl.DateTimeFormat = function(locales, options) {
|
Intl.DateTimeFormat = function(locales, options) {
|
||||||
if (!options) options = {};
|
if (!options) options = {};
|
||||||
if (!options.timeZone) options.timeZone = _defaultTZ;
|
if (!options.timeZone) options.timeZone = _defaultTZ;
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ fn op_dom(state: &OpState, #[string] cmd: String, #[string] arg1: String, #[stri
|
|||||||
match &mut n.data {
|
match &mut n.data {
|
||||||
NodeData::Text { contents } => { *contents = arg2.clone(); }
|
NodeData::Text { contents } => { *contents = arg2.clone(); }
|
||||||
NodeData::Comment { contents } => { *contents = arg2.clone(); }
|
NodeData::Comment { contents } => { *contents = arg2.clone(); }
|
||||||
|
NodeData::ProcessingInstruction { data, .. } => { *data = arg2.clone(); }
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -279,6 +280,47 @@ fn op_dom(state: &OpState, #[string] cmd: String, #[string] arg1: String, #[stri
|
|||||||
"create_comment_node" => {
|
"create_comment_node" => {
|
||||||
dom.new_node(NodeData::Comment { contents: arg1.clone() }).index().to_string()
|
dom.new_node(NodeData::Comment { contents: arg1.clone() }).index().to_string()
|
||||||
}
|
}
|
||||||
|
"create_processing_instruction" => {
|
||||||
|
// arg1 = target, arg2 = data
|
||||||
|
dom.new_node(NodeData::ProcessingInstruction {
|
||||||
|
target: arg1.clone(),
|
||||||
|
data: arg2.clone(),
|
||||||
|
}).index().to_string()
|
||||||
|
}
|
||||||
|
"create_doctype" => {
|
||||||
|
// arg1 = name, arg2 = public_id. system_id stored only in the
|
||||||
|
// JS wrapper since neither current WPT test reads it back from
|
||||||
|
// the underlying tree.
|
||||||
|
dom.new_node(NodeData::Doctype {
|
||||||
|
name: arg1.clone(),
|
||||||
|
public_id: arg2.clone(),
|
||||||
|
system_id: String::new(),
|
||||||
|
}).index().to_string()
|
||||||
|
}
|
||||||
|
"pi_target" => {
|
||||||
|
let nid = arg1.parse::<u32>().unwrap_or(0);
|
||||||
|
let val = dom.get_node(NodeId::new(nid)).and_then(|n| match &n.data {
|
||||||
|
NodeData::ProcessingInstruction { target, .. } => Some(target.clone()),
|
||||||
|
_ => None,
|
||||||
|
}).unwrap_or_default();
|
||||||
|
serde_json::to_string(&val).unwrap_or("\"\"".into())
|
||||||
|
}
|
||||||
|
"doctype_name" => {
|
||||||
|
let nid = arg1.parse::<u32>().unwrap_or(0);
|
||||||
|
let val = dom.get_node(NodeId::new(nid)).and_then(|n| match &n.data {
|
||||||
|
NodeData::Doctype { name, .. } => Some(name.clone()),
|
||||||
|
_ => None,
|
||||||
|
}).unwrap_or_default();
|
||||||
|
serde_json::to_string(&val).unwrap_or("\"\"".into())
|
||||||
|
}
|
||||||
|
"doctype_public_id" => {
|
||||||
|
let nid = arg1.parse::<u32>().unwrap_or(0);
|
||||||
|
let val = dom.get_node(NodeId::new(nid)).and_then(|n| match &n.data {
|
||||||
|
NodeData::Doctype { public_id, .. } => Some(public_id.clone()),
|
||||||
|
_ => None,
|
||||||
|
}).unwrap_or_default();
|
||||||
|
serde_json::to_string(&val).unwrap_or("\"\"".into())
|
||||||
|
}
|
||||||
"element_children" => {
|
"element_children" => {
|
||||||
let nid = arg1.parse::<u32>().unwrap_or(0);
|
let nid = arg1.parse::<u32>().unwrap_or(0);
|
||||||
let ids: Vec<i32> = dom.children(NodeId::new(nid)).iter()
|
let ids: Vec<i32> = dom.children(NodeId::new(nid)).iter()
|
||||||
@@ -726,7 +768,7 @@ fn validate_fetch_url(url: &url::Url) -> Result<(), String> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if scheme == "file" {
|
if scheme == "file" || obscura_net::env_allows_private_network() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user