slicc-cdp-client 1.41.8

Shared CDP connection for slicc-cdp CLIs
Documentation
use super::*;

const MAX_FRAMES: usize = 4096;
const MAX_TARGETS: usize = 256;

pub(super) fn content(method: &str) -> bool {
    ["DOM.", "CSS.", "DOMSnapshot.", "Accessibility.", "Input."].iter().any(|prefix| method.starts_with(prefix))
        || matches!(method, "Page.captureScreenshot" | "Page.printToPDF" | "Page.startScreencast")
}

impl Cdp {
    pub(super) fn verify_page_scope(&mut self, session: Option<&str>) -> Result<(String, u64, u64), String> {
        let target = session.and_then(|session| self.session_target(session)).ok_or_else(|| self.refusal())?;
        let listed = self.raw_call("Target.getTargets", json!({"filter":[{}]}), None, DOCUMENT_CHECK).map_err(|_| self.refusal())?;
        let infos = listed.get("targetInfos").and_then(Value::as_array).ok_or_else(|| self.refusal())?;
        let selected = infos.iter().find(|info| info.get("targetId").and_then(Value::as_str) == Some(&target)).ok_or_else(|| self.refusal())?;
        if !matches!(selected.get("type").and_then(Value::as_str), Some("page" | "iframe")) { return Err(self.refusal()) }
        let context = selected.get("browserContextId").cloned();
        let has_iframes = infos.iter().any(|info| info.get("browserContextId") == context.as_ref() && info.get("type").and_then(Value::as_str) == Some("iframe"));
        let infos: Vec<Value> = infos.iter().filter(|info| info.get("browserContextId") == context.as_ref()
            && matches!(info.get("type").and_then(Value::as_str), Some("page" | "iframe"))
            && (has_iframes || info.get("targetId").and_then(Value::as_str) == Some(&target))).cloned().collect();
        if infos.len() > MAX_TARGETS { return Err(self.refusal()) }
        let mut ids = BTreeSet::new();
        if infos.iter().any(|info| info.get("targetId").and_then(Value::as_str).is_none_or(|id| !ids.insert(id))) { return Err(self.refusal()) }
        let epochs = self.page_scope_epochs.clone();
        let unknown = self.page_scope_unknown;
        for info in &infos {
            if let Some(id) = info.get("targetId").and_then(Value::as_str) { self.targets.insert(id.to_string(), info.clone()); }
        }
        let mut nodes = BTreeMap::new();
        let mut roots = BTreeMap::new();
        let mut target_roots = BTreeMap::new();
        let mut iframe_roots = Vec::new();
        let mut protected_iframes = Vec::new();
        let deadline = Instant::now() + DOCUMENT_CHECK;
        for info in &infos {
            let id = info.get("targetId").and_then(Value::as_str).filter(|id| frame_text_known(id)).ok_or_else(|| self.refusal())?;
            let url = info.get("url").and_then(Value::as_str).ok_or_else(|| self.refusal())?;
            let iframe = info.get("type").and_then(Value::as_str) == Some("iframe");
            let error_probe = self.guard.error_page_url(url) && self.guards.contains_key(id) && self.documents.get(id).is_some_and(|doc| !doc.previous);
            let protected = !frame_text_known(url) || !error_probe && self.info_protected(info, &[]) || self.protected_targets.contains(id);
            if protected {
                let parent = if iframe { Some(info.get("parentFrameId").and_then(Value::as_str).filter(|id| frame_text_known(id)).ok_or_else(|| self.refusal())?) } else { None };
                nodes.entry(id.to_string()).or_insert_with(|| json!({"id":id,"parentId":parent,"url":url,"securityOrigin":url,"loaderId":"protected"}));
                if iframe { iframe_roots.push(id.to_string()); protected_iframes.push(id.to_string()); } else { roots.insert(id.to_string(), id.to_string()); }
                continue;
            }
            let mut tree = self.page_scope_tree(id, deadline)?;
            let frame = tree.pointer_mut("/frameTree/frame").ok_or_else(|| self.refusal())?;
            let root = frame.get("id").and_then(Value::as_str).filter(|id| frame_text_known(id)).ok_or_else(|| self.refusal())?.to_string();
            if iframe {
                let parent = info.get("parentFrameId").and_then(Value::as_str).filter(|id| frame_text_known(id)).ok_or_else(|| self.refusal())?;
                if frame.get("parentId").is_some_and(|id| id.as_str() != Some(parent)) { return Err(self.refusal()) }
                frame["parentId"] = json!(parent);
                iframe_roots.push(root.clone());
                if self.protected_targets.contains(id) { protected_iframes.push(root.clone()); }
            } else {
                if frame.get("parentId").is_some() || roots.insert(root.to_string(), id.to_string()).is_some() { return Err(self.refusal()) }
            }
            target_roots.insert(id.to_string(), root);
            Self::page_scope_nodes(tree.get("frameTree").unwrap(), None, &mut nodes).ok_or_else(|| self.refusal())?;
        }
        let owner = |id: &str| -> Option<String> {
            let mut current = id;
            let mut visited = BTreeSet::new();
            while visited.insert(current.to_string()) {
                if let Some(owner) = roots.get(current) { return Some(owner.clone()) }
                current = nodes.get(current)?.get("parentId")?.as_str()?;
            }
            None
        };
        for id in &iframe_roots { owner(id).ok_or_else(|| self.refusal())?; }
        for id in nodes.keys() {
            if let Some(page) = owner(id) { self.page_scope_owners.insert(id.clone(), page); }
        }
        for (target, root) in &target_roots {
            if let Some(page) = owner(root) { self.page_scope_owners.insert(target.clone(), page); }
        }
        for info in &infos {
            let id = info.get("targetId").and_then(Value::as_str).unwrap();
            if info.get("type").and_then(Value::as_str) == Some("page") { self.page_scope_owners.insert(id.to_string(), id.to_string()); }
        }
        let page = if selected.get("type").and_then(Value::as_str) == Some("page") { target.clone() } else { owner(target_roots.get(&target).map(String::as_str).unwrap_or(&target)).ok_or_else(|| self.refusal())? };
        if protected_iframes.iter().any(|id| owner(id).as_deref() == Some(&page)) { return Err(self.refusal()) }
        let root = roots.iter().find(|(_, owner)| *owner == &page).map(|(id, _)| id).ok_or_else(|| self.refusal())?;
        let tree = Self::page_scope_join(root, &nodes, &mut BTreeSet::new()).ok_or_else(|| self.refusal())?;
        let mut frames = BTreeMap::new();
        let epoch = epochs.get(&page).copied().unwrap_or(0);
        if epoch != self.page_scope_epochs.get(&page).copied().unwrap_or(0) || unknown != self.page_scope_unknown || !self.collect_document_frames(&tree, &mut frames, true, None)
            || frames.iter().any(|(id, frame)| {
                let ordinary_error = self.guard.error_document(&frame[1], &frame[2]) && !self.unverified_error(&frame[1], &frame[3])
                    && if frame[4].is_empty() { self.documents.get(&page).is_some_and(|doc| !doc.previous) } else { self.frame_decisions.get(id) == Some(&false) };
                !ordinary_error && (self.guard.url(&frame[1]) || self.guard.url(&frame[2]))
            }) { return Err(self.refusal()) }
        for (id, evidence) in &frames {
            self.frames.insert(id.clone(), vec![evidence[1].clone(), evidence[2].clone()]);
            self.note_parent(id, &evidence[4]);
        }
        Ok((page, epoch, unknown))
    }

    fn page_scope_tree(&mut self, target: &str, deadline: Instant) -> Result<Value, String> {
        let left = deadline.saturating_duration_since(Instant::now());
        if left.is_zero() { return Err(self.refusal()) }
        let session = match self.guards.get(target).or_else(|| self.page_scope_sessions.get(target)).cloned() {
            Some(session) => session,
            None => {
                self.guarding.insert(target.to_string());
                let attached = self.raw_call("Target.attachToTarget", json!({"targetId":target,"flatten":true}), None, left);
                self.guarding.remove(target);
                let result = attached.map_err(|_| self.refusal())?;
                let session = result.get("sessionId").and_then(Value::as_str).filter(|id| !id.is_empty()).ok_or_else(|| self.refusal())?.to_string();
                self.guard_sessions.insert(session.clone());
                self.session_targets.insert(session.clone(), target.to_string());
                self.page_scope_sessions.insert(target.to_string(), session.clone());
                let left = deadline.saturating_duration_since(Instant::now());
                if left.is_zero() || self.raw_call("Page.enable", json!({}), Some(&session), left).is_err() {
                    self.page_scope_sessions.remove(target);
                    let _ = self.send("Target.detachFromTarget", json!({"sessionId":session}), None);
                    self.guard_sessions.remove(&session);
                    self.session_targets.remove(&session);
                    return Err(self.refusal());
                }
                session
            }
        };
        let left = deadline.saturating_duration_since(Instant::now());
        if left.is_zero() { return Err(self.refusal()) }
        let mut tree = self.raw_call("Page.getFrameTree", json!({}), Some(&session), left).map_err(|_| self.refusal())?;
        if self.page_scope_versions(target, &tree)? {
            self.context_proofs.retain(|(owner, _), _| owner != &session);
            for method in ["Runtime.disable", "Runtime.enable"] {
                let left = deadline.saturating_duration_since(Instant::now());
                if left.is_zero() { return Err(self.refusal()) }
                if self.raw_call(method, json!({}), Some(&session), left).is_err() {
                    if self.protected_targets.contains(target) { return Ok(tree) }
                    return Err(self.refusal());
                }
            }
            let left = deadline.saturating_duration_since(Instant::now());
            if left.is_zero() { return Err(self.refusal()) }
            tree = self.raw_call("Page.getFrameTree", json!({}), Some(&session), left).map_err(|_| self.refusal())?;
        }
        Ok(tree)
    }

    fn page_scope_nodes(node: &Value, parent: Option<&str>, nodes: &mut BTreeMap<String, Value>) -> Option<()> {
        let frame = node.get("frame")?;
        let id = frame.get("id")?.as_str().filter(|id| frame_text_known(id))?;
        if nodes.len() >= MAX_FRAMES || parent.is_some_and(|parent| frame.get("parentId").is_some_and(|id| id.as_str() != Some(parent))) { return None }
        let mut frame = frame.clone();
        if let Some(parent) = parent { frame["parentId"] = json!(parent); }
        if let Some(previous) = nodes.get(id) {
            if previous.get("parentId") != frame.get("parentId") { return None }
        }
        nodes.insert(id.to_string(), frame);
        if let Some(children) = node.get("childFrames") {
            for child in children.as_array()? { Self::page_scope_nodes(child, Some(id), nodes)?; }
        }
        Some(())
    }

    fn page_scope_join(id: &str, nodes: &BTreeMap<String, Value>, visited: &mut BTreeSet<String>) -> Option<Value> {
        if !visited.insert(id.to_string()) { return None }
        let frame = nodes.get(id)?;
        let mut children = Vec::new();
        for (child, frame) in nodes {
            if frame.get("parentId").and_then(Value::as_str) == Some(id) { children.push(Self::page_scope_join(child, nodes, visited)?); }
        }
        Some(json!({"frame":frame,"childFrames":children}))
    }

    fn page_scope_versions(&mut self, target: &str, tree: &Value) -> Result<bool, String> {
        let root = tree.get("frameTree").ok_or_else(|| self.refusal())?;
        let mut queue = vec![(root, false)];
        let mut inherited = false;
        let mut count = 0;
        while let Some((node, child)) = queue.pop() {
            count += 1;
            if count > MAX_FRAMES { return Err(self.refusal()) }
            let frame = node.get("frame").ok_or_else(|| self.refusal())?;
            let id = frame.get("id").and_then(Value::as_str).ok_or_else(|| self.refusal())?;
            if let Some(loader) = frame.get("loaderId").and_then(Value::as_str) {
                self.document_versions.insert((target.to_string(), id.to_string()), loader.to_string());
            }
            inherited |= (child || frame.get("parentId").is_some()) && frame.get("securityOrigin").and_then(Value::as_str) == Some("://")
                && self.guard.inheriting(frame.get("url").and_then(Value::as_str).unwrap_or("")) && self.page_scope_origin(frame).is_none();
            if let Some(children) = node.get("childFrames") {
                for child in children.as_array().ok_or_else(|| self.refusal())? { queue.push((child, true)); }
            }
        }
        Ok(inherited)
    }

    pub(super) fn page_scope_origin<'a>(&'a self, frame: &Value) -> Option<&'a str> {
        if frame.get("securityOrigin").and_then(Value::as_str) != Some("://")
            || !self.guard.inheriting(frame.get("url").and_then(Value::as_str).unwrap_or("")) { return None }
        let id = frame.get("id")?.as_str()?;
        let loader = frame.get("loaderId")?.as_str()?;
        self.context_proofs.iter().find(|((session, _), proof)| {
            self.guard_sessions.contains(session) && proof.default && proof.frame == id && proof.loader.as_deref() == Some(loader)
                && frame_text_known(&proof.origin)
        }).map(|(_, proof)| proof.origin.as_str())
    }

    pub(super) fn page_scope_owner(&self, id: &str) -> Option<String> {
        let mut current = id;
        let mut visited = BTreeSet::new();
        while visited.insert(current.to_string()) && visited.len() <= MAX_FRAMES {
            if let Some(page) = self.page_scope_owners.get(current) { return Some(page.clone()) }
            if let Some(info) = self.targets.get(current) {
                if info.get("type").and_then(Value::as_str) == Some("page") { return Some(current.to_string()) }
                if let Some(parent) = info.get("parentFrameId").and_then(Value::as_str) { current = parent; continue; }
            }
            current = self.frame_parents.get(current)?;
        }
        None
    }

    pub(super) fn page_scope_changed(&mut self, method: &str, params: &Value, session: Option<&str>) {
        let mut pages = BTreeSet::new();
        if matches!(method, "Page.frameNavigated" | "Page.frameAttached" | "Page.frameDetached" | "Page.navigatedWithinDocument") {
            if method == "Page.frameNavigated" {
                let frame = &params["frame"];
                let id = frame.get("id").and_then(Value::as_str).unwrap_or("");
                if self.frames.get(id).is_some_and(|known| known.first() == frame.get("url").and_then(Value::as_str).map(String::from).as_ref()
                    && known.get(1) == frame.get("securityOrigin").and_then(Value::as_str).map(String::from).as_ref())
                    && session.and_then(|session| self.session_target(session)).is_some_and(|target| self.document_versions.get(&(target, id.to_string())).map(String::as_str) == frame.get("loaderId").and_then(Value::as_str)) { return }
            }
            if let Some(page) = session.and_then(|session| self.session_target(session)).and_then(|target| self.page_scope_owner(&target)) { pages.insert(page); }
        } else if matches!(method, "Target.targetCreated" | "Target.targetInfoChanged" | "Target.targetDestroyed") {
            let info = params.get("targetInfo");
            let id = info.and_then(|info| info.get("targetId")).or_else(|| params.get("targetId")).and_then(Value::as_str).unwrap_or("");
            let previous = self.targets.get(id);
            if info.or(previous).and_then(|info| info.get("type")).and_then(Value::as_str) != Some("iframe") { return }
            if info.is_some_and(|info| previous.is_some_and(|previous| ["url", "parentFrameId"].iter().all(|key| previous.get(*key) == info.get(*key)))) { return }
            if let Some(page) = self.page_scope_owner(id) { pages.insert(page); }
            if let Some(page) = info.and_then(|info| info.get("parentFrameId")).and_then(Value::as_str).and_then(|parent| self.page_scope_owner(parent)) { pages.insert(page); }
        } else { return }
        if pages.is_empty() { self.page_scope_unknown += 1; }
        for page in pages { *self.page_scope_epochs.entry(page).or_default() += 1; }
    }
}