slicc-cdp-client 1.42.3

Shared CDP connection for slicc-cdp CLIs
Documentation
struct AttachBrowser {
    url: String,
    main: Value,
    during_read: Option<Value>,
    attaching: bool,
}

struct AttachTree {
    inbox: VecDeque<String>,
    browser: std::rc::Rc<std::cell::RefCell<AttachBrowser>>,
    guard: bool,
}

impl Transport for AttachTree {
    fn send_text(&mut self, text: &str) -> Result<(), String> {
        let call: Value = serde_json::from_str(text).unwrap();
        let mut browser = self.browser.borrow_mut();
        let result = match call["method"].as_str().unwrap() {
            "Target.attachToTarget" => {
                let session = if self.guard { "G" } else { "U" };
                self.guard = true;
                json!({ "sessionId": session })
            }
            "Target.getTargetInfo" => json!({ "targetInfo": { "targetId": "T", "type": "page", "url": browser.url, "title": "" } }),
            "Page.getFrameTree" => {
                let event = if browser.attaching { browser.during_read.clone() } else { browser.during_read.take() };
                if let Some(event) = event {
                    self.inbox.push_back(event.to_string());
                }
                json!({ "frameTree": { "frame": browser.main } })
            }
            "Runtime.evaluate" => json!({ "result": { "type": "string", "value": "complete" } }),
            _ => json!({}),
        };
        self.inbox.push_back(json!({ "id": call["id"], "sessionId": call["sessionId"], "result": result }).to_string());
        Ok(())
    }

    fn recv_text(&mut self, _deadline: Instant) -> Result<Option<String>, String> {
        Ok(self.inbox.pop_front())
    }
}

const ATTACH_LANDING: &str = "http://127.0.0.1:8000/landing?n=window";
const ATTACH_SEVEN: &str = "http://seven.sliccy.ai/dump?n=main";

fn attach_during(during_read: Option<Value>) -> (Cdp, String, std::rc::Rc<std::cell::RefCell<AttachBrowser>>) {
    let browser = std::rc::Rc::new(std::cell::RefCell::new(AttachBrowser {
        url: ATTACH_LANDING.into(),
        main: json!({ "id": "T", "url": ATTACH_LANDING, "securityOrigin": "http://127.0.0.1:8000" }),
        during_read,
        attaching: true,
    }));
    let mut cdp = Cdp::from_transport(Box::new(AttachTree { inbox: VecDeque::new(), browser: browser.clone(), guard: false }));
    let session = super::attach(&mut cdp, "T").unwrap();
    browser.borrow_mut().attaching = false;
    browser.borrow_mut().during_read = None;
    assert_eq!(cdp.guards["T"], "G");
    (cdp, session, browser)
}

fn blocked_seven_navigation(cdp: &mut Cdp, browser: &std::cell::RefCell<AttachBrowser>) {
    let error = json!({ "id": "T", "url": "chrome-error://chromewebdata/", "securityOrigin": "://", "unreachableUrl": ATTACH_SEVEN });
    cdp.absorb(json!({ "method": "Page.frameStartedNavigating", "sessionId": "G", "params": { "frameId": "T", "url": ATTACH_SEVEN, "navigationType": "differentDocument" } }));
    cdp.absorb(json!({ "method": "Page.frameNavigated", "sessionId": "G", "params": { "frame": error, "type": "Navigation" } }));
    cdp.absorb(json!({ "method": "Target.targetInfoChanged", "params": { "targetInfo": { "targetId": "T", "type": "page", "url": ATTACH_SEVEN, "title": "seven.sliccy.ai" } } }));
    let mut browser = browser.borrow_mut();
    browser.url = ATTACH_SEVEN.into();
    browser.main = error;
}

fn ready(cdp: &mut Cdp, session: &str) -> Result<Value, String> {
    cdp.call("Runtime.evaluate", json!({ "expression": "document.readyState", "returnByValue": true }), Some(session))
}

#[test]
fn a_subframe_attached_while_the_guard_reads_the_tree_keeps_the_main_document() {
    for during_read in [None, Some(json!({ "method": "Page.frameAttached", "sessionId": "G", "params": { "frameId": "CHILD", "parentFrameId": "T" } }))] {
        let (mut cdp, session, browser) = attach_during(during_read.clone());
        assert_eq!(cdp.document_evidence("T"), format!("url={ATTACH_LANDING} unreachable= navigating=false previous=false probed=false"), "{during_read:?}");
        blocked_seven_navigation(&mut cdp, &browser);
        assert_eq!(ready(&mut cdp, &session).unwrap()["result"]["value"], "complete", "{during_read:?}");
        assert!(!cdp.session_blocked(&session), "{during_read:?}");
        assert!(!cdp.target_protected("T", false).unwrap(), "{during_read:?}");
        assert!(cdp.call("Runtime.evaluate", json!({ "expression": "document.cookie" }), Some(&session)).is_err());
        assert!(cdp.call("Page.navigate", json!({ "url": ATTACH_SEVEN }), Some(&session)).is_err());
        assert!(!cdp.session_blocked(&session));
    }
}

#[test]
fn a_main_frame_update_during_the_guard_tree_read_still_fails_closed() {
    let update = json!({ "method": "Page.navigatedWithinDocument", "sessionId": "G", "params": { "frameId": "T", "url": format!("{ATTACH_LANDING}#moved") } });
    let (mut cdp, session, browser) = attach_during(Some(update));
    assert_eq!(cdp.document_evidence("T"), "none");
    blocked_seven_navigation(&mut cdp, &browser);
    assert!(cdp.session_blocked(&session));
    assert!(cdp.target_protected("T", false).unwrap());
    assert!(ready(&mut cdp, &session).is_err());
}

#[test]
fn a_callers_tree_read_racing_a_subframe_attach_is_still_refused() {
    let (mut cdp, session, browser) = attach_during(None);
    browser.borrow_mut().during_read = Some(json!({ "method": "Page.frameAttached", "sessionId": "G", "params": { "frameId": "LATE", "parentFrameId": "T" } }));
    assert!(cdp.call("Page.getFrameTree", json!({}), Some(&session)).unwrap_err().starts_with("slicc: refused: "));
    assert!(cdp.call("Page.getFrameTree", json!({}), Some(&session)).is_ok());
    assert!(!cdp.session_blocked(&session));
}