Skip to main content

spire_thirtyfour/session/
mod.rs

1mod document;
2mod navigation;
3
4use std::fmt;
5use std::ops::Deref;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::time::Duration;
8
9use bytes::Bytes;
10use deadpool::managed::Object;
11use futures::future::BoxFuture;
12use spire_core::backend::document::{Document, DocumentLinks};
13use spire_core::backend::{Session, StatusPolicy};
14use spire_core::http::http::header::{
15    CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, HeaderValue, TRANSFER_ENCODING,
16};
17use spire_core::http::{Request, Response, ResponseExt};
18use spire_core::{Error, Result};
19use thirtyfour::WebDriver;
20use thirtyfour::bidi::{BiDi, BrowsingContextId};
21
22use self::navigation::{Overrides, navigate};
23use crate::backend::pool::Sessions;
24use crate::error::from_webdriver;
25
26/// A pooled WebDriver session, handed out for one request.
27///
28/// Each request gets a tab of its own, in a fresh browser profile unless the
29/// backend keeps session state, so cookies and storage do not leak between
30/// requests. [`fetch`](Session::fetch) navigates the tab to the request URI,
31/// returning a complete response with a fixed HTML snapshot. Later changes
32/// are read through [`page_source`](Self::page_source) or the live
33/// [`Document`](spire_core::backend::document::Document) capability.
34/// Through [`Deref`], the session also exposes the full [`WebDriver`] API,
35/// pointed at the request's tab; it returns to the pool when dropped.
36///
37/// Navigation goes through WebDriver BiDi, so responses carry the page's
38/// real status and headers, and requests are sent with their method,
39/// headers and body.
40pub struct ThirtyfourSession {
41    driver: Object<Sessions>,
42    bidi: BiDi,
43    context: BrowsingContextId,
44    page_load_timeout: Duration,
45    status_policy: StatusPolicy,
46    /// Whether a navigation reached a page, so there is a body to read.
47    navigated: AtomicBool,
48}
49
50impl ThirtyfourSession {
51    pub(crate) const fn new(
52        driver: Object<Sessions>,
53        bidi: BiDi,
54        context: BrowsingContextId,
55        page_load_timeout: Duration,
56        status_policy: StatusPolicy,
57    ) -> Self {
58        Self {
59            driver,
60            bidi,
61            context,
62            page_load_timeout,
63            status_policy,
64            navigated: AtomicBool::new(false),
65        }
66    }
67
68    /// Returns a handle to the session's [`WebDriver`].
69    ///
70    /// The handle stays valid while the session is in use; once the session
71    /// returns to the pool it may be reset or handed to another request.
72    #[must_use]
73    pub fn driver(&self) -> WebDriver {
74        (*self.driver).clone()
75    }
76
77    /// Returns the session's WebDriver BiDi connection, e.g. to listen to
78    /// console logs or network events.
79    #[must_use]
80    pub const fn bidi(&self) -> &BiDi {
81        &self.bidi
82    }
83
84    /// Returns the browsing context, the tab, the request is handled in.
85    #[must_use]
86    pub const fn context(&self) -> &BrowsingContextId {
87        &self.context
88    }
89
90    /// Returns the source of the page as currently rendered, including
91    /// changes made by scripts and interaction since it loaded.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the browser cannot be reached.
96    pub async fn page_source(&self) -> Result<String> {
97        self.source().await.map_err(from_webdriver)
98    }
99}
100
101impl Deref for ThirtyfourSession {
102    type Target = WebDriver;
103
104    fn deref(&self) -> &Self::Target {
105        &self.driver
106    }
107}
108
109impl fmt::Debug for ThirtyfourSession {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.debug_struct("ThirtyfourSession")
112            .field("context", &self.context)
113            .finish_non_exhaustive()
114    }
115}
116
117impl Session for ThirtyfourSession {
118    /// Navigates the tab to the request URI, sent with its method, headers
119    /// and body, and returns the head of the response the page ended on
120    /// after redirects, checked against the backend's [`StatusPolicy`].
121    ///
122    /// The head has the page's status and headers, but the body is the
123    /// rendered document serialized as HTML, so its content type is always
124    /// `text/html; charset=utf-8`, whatever the server sent.
125    async fn fetch(&self, request: &Request) -> Result<Response> {
126        let overrides = Overrides {
127            method: request.method().clone(),
128            headers: request.headers().clone(),
129            body: request.body().to_vec(),
130        };
131        let navigation = navigate(
132            &self.bidi,
133            &self.context,
134            request.uri(),
135            &overrides,
136            self.page_load_timeout,
137        )
138        .await?;
139
140        let mut head = Response::new(());
141        *head.status_mut() = navigation.status();
142        let headers = head.headers_mut();
143        *headers = navigation.headers().clone();
144        for header in [CONTENT_LENGTH, CONTENT_ENCODING, TRANSFER_ENCODING] {
145            headers.remove(header);
146        }
147        let content_type = HeaderValue::from_static("text/html; charset=utf-8");
148        headers.insert(CONTENT_TYPE, content_type);
149        head.set_final_uri(navigation.uri().clone());
150        self.navigated.store(true, Ordering::Release);
151        let head = self.status_policy.check(head)?;
152        let body = Bytes::from(self.page_source().await?);
153        Ok(head.map(|()| body))
154    }
155
156    /// Returns the live DOM capability, separate from the response snapshot.
157    fn document(&self) -> Option<&dyn Document> {
158        Some(self)
159    }
160}
161
162impl Document for ThirtyfourSession {
163    fn links<'a>(&'a self, selector: Option<&'a str>) -> BoxFuture<'a, Result<DocumentLinks>> {
164        Box::pin(async move {
165            if !self.navigated.load(Ordering::Acquire) {
166                return Err(Error::worker("the request was not fetched yet"));
167            }
168            self::document::capture(self, selector).await
169        })
170    }
171}