Skip to main content

glass/browser/session/
download.rs

1//! Scoped download lifecycle management.
2//!
3//! Waits for and captures a single file download initiated by the browser,
4//! returning the downloaded bytes. Only one download may be authorized at a
5//! time per session.
6
7use super::*;
8
9impl BrowserSession {
10    /// Wait for one explicitly authorized download lifecycle.
11    pub async fn wait_for_download(
12        &self,
13        destination: &Path,
14        deadline: Duration,
15    ) -> BrowserResult<DownloadOutcome> {
16        self.policy.require(PolicyCapability::Download)?;
17        if deadline.is_zero() || deadline > MAX_DIAGNOSTIC_DURATION {
18            return Err("download deadline must be between 1 ms and 30 seconds".into());
19        }
20        let destination = self.policy.require_existing_path(destination)?;
21        if !destination.is_dir() || !destination.starts_with(&self.upload_root) {
22            return Err(
23                "download destination must be a directory inside the authorized root".into(),
24            );
25        }
26        let (target_id, frame_id) = self.route_identity().await?;
27        let page_session_id = if use_page_download_compatibility(
28            self.chrome.is_some(),
29            self.disposable_profile.is_some(),
30        ) {
31            let topology = self.topology.lock().await;
32            if topology.active_target_id.as_deref() != Some(target_id.as_str()) {
33                return Err(download_error(
34                    DownloadErrorKind::AuthorizationFailed,
35                    "incognito download route changed during capture",
36                )
37                .into());
38            }
39            Some(topology.active_target_session_id.clone().ok_or_else(|| {
40                download_error(
41                    DownloadErrorKind::AuthorizationFailed,
42                    "incognito download has no captured top-level page session",
43                )
44            })?)
45        } else {
46            None
47        };
48        let _download_scope = self.download_scope.lock().await;
49        let mut events = self.cdp.subscribe_events_with_params();
50        let mut download_guard = match page_session_id {
51            Some(page_session_id) => {
52                DownloadBehaviorGuard::acquire_for_incognito(
53                    self.cdp.clone(),
54                    destination.clone(),
55                    target_id.clone(),
56                    page_session_id,
57                    self.launched_incognito_context_id.clone().ok_or_else(|| {
58                        download_error(
59                            DownloadErrorKind::AuthorizationFailed,
60                            "owned incognito session has no original browser context ID",
61                        )
62                    })?,
63                )
64                .await?
65            }
66            None => {
67                DownloadBehaviorGuard::acquire(self.cdp.clone(), destination.clone(), None).await?
68            }
69        };
70        let result = tokio::time::timeout(deadline, async {
71            let mut guid = None;
72            let mut filename = String::new();
73            loop {
74                match events.recv().await {
75                    Ok(event) if event.method == "Browser.downloadWillBegin" => {
76                        if event.params["frameId"].as_str() != Some(frame_id.as_str()) {
77                            continue;
78                        }
79                        guid = event.params["guid"].as_str().map(bounded_diagnostic_text);
80                        filename = bounded_diagnostic_text(
81                            event.params["suggestedFilename"]
82                                .as_str()
83                                .unwrap_or("download"),
84                        );
85                    }
86                    Ok(event) if event.method == "Browser.downloadProgress" => {
87                        let Some(active_guid) = guid.as_deref() else {
88                            continue;
89                        };
90                        if event.params["guid"].as_str() != Some(active_guid) {
91                            continue;
92                        }
93                        let state = event.params["state"].as_str().unwrap_or("inProgress");
94                        if matches!(state, "completed" | "canceled") {
95                            self.download_sequence.fetch_add(1, Ordering::Relaxed);
96                            self.record_audit(
97                                "download",
98                                format!("{} (state={})", filename, state),
99                            );
100                            return BrowserResult::Ok(DownloadOutcome {
101                                guid: active_guid.to_string(),
102                                suggested_filename: filename,
103                                state: state.to_ascii_lowercase(),
104                                received_bytes: finite_nonnegative_u64(
105                                    &event.params["receivedBytes"],
106                                ),
107                                total_bytes: finite_nonnegative_u64(&event.params["totalBytes"]),
108                                target_id: target_id.clone(),
109                                frame_id: frame_id.clone(),
110                                sha256: None,
111                            });
112                        }
113                    }
114                    Ok(_) => {}
115                    Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
116                        return Err(format!("download event stream dropped {count} events").into());
117                    }
118                    Err(tokio::sync::broadcast::error::RecvError::Closed) => {
119                        return Err("download event stream closed".into());
120                    }
121                }
122            }
123        })
124        .await
125        .unwrap_or_else(|_| Err("download deadline exceeded".into()));
126        download_guard.disable().await?;
127        result
128    }
129}