Skip to main content

cdp_server/
lib.rs

1// @trace REQ-CDS-006 [entity:DomainRegistry] [entity:ServerConfig]
2// @trace REQ-CDS-008 [entity:ServerConfig]
3// cdp-server — Generic CDP (Chrome DevTools Protocol) server framework.
4// Transport layer (HTTP discovery + WebSocket) + session management +
5// message routing + event broadcast + domain handler registry.
6// Zero knowledge of any browser engine.
7
8use serde_json::Value;
9
10pub mod bao_event;
11mod event;
12mod protocol;
13mod registry;
14mod server;
15mod session;
16mod transport;
17
18pub use bao_event::{BaoEvent, ConsoleMessage};
19pub use event::EventBroadcaster;
20pub use protocol::{
21    error_response, ok_empty, ok_response, parse_message, serialize_event, serialize_response,
22    CdpError, CdpEvent, CdpMessage, CdpResponse, SessionError, ERR_INTERNAL, ERR_INVALID_PARAMS,
23    ERR_INVALID_REQUEST, ERR_METHOD_NOT_FOUND, ERR_PARSE_ERROR,
24};
25pub use registry::{DomainRegistry, EmptyHandler, RegistryDispatch, SharedRegistry};
26pub use server::CdpServer;
27pub use session::{CdpSession, SessionHandle, SessionState};
28pub use transport::{
29    is_websocket_upgrade, parse_activate_request, parse_close_request, parse_new_request,
30    TargetInfo,
31};
32
33// ---------------------------------------------------------------------------
34// §2.1 DomainHandler Trait
35// ---------------------------------------------------------------------------
36
37/// CDP Domain handler trait. Each implementation handles one CDP domain
38/// (e.g. Page, Runtime, DOM). All browser-specific logic lives here.
39///
40/// Constraints: `Send + Sync` (cross-thread safe).
41pub trait DomainHandler: Send + Sync {
42    /// Returns the CDP domain name (e.g. "Page", "Runtime").
43    fn domain_name(&self) -> &'static str;
44
45    /// Handle a CDP command. `command` is the full method string
46    /// (e.g. "Page.navigate"). `params` is the JSON params object.
47    fn handle_command(
48        &self,
49        command: &str,
50        params: Value,
51        event_sender: &dyn EventSender,
52    ) -> Result<Value, CdpError>;
53
54    /// Called when a session enables this domain for the first time.
55    fn on_session_created(&self, _session_id: &str) {}
56
57    /// Called when a session is destroyed (while this domain was enabled).
58    fn on_session_destroyed(&self, _session_id: &str) {}
59}
60
61// ---------------------------------------------------------------------------
62// §2.2 EventSender Trait
63// ---------------------------------------------------------------------------
64
65/// Event sender trait. Implemented internally by cdp-server, injected into
66/// DomainHandlers so they can broadcast CDP events.
67///
68/// Constraints: `Send + Sync + Clone`.
69pub trait EventSender: Send + Sync {
70    /// Broadcast an event to all sessions that have enabled the domain
71    /// extracted from `method` (format: "Domain.eventName").
72    fn send_event(&self, method: &str, params: Value);
73
74    /// Broadcast a session-scoped event (flattened CDP sessions): the event
75    /// JSON carries `sessionId`, so clients route it to the attached target
76    /// session. Default impl degrades to a plain broadcast (registries
77    /// without session knowledge lose only the routing tag).
78    fn send_session_event(&self, session_id: &str, method: &str, params: Value) {
79        let _ = (session_id, method, params);
80    }
81}
82
83// ---------------------------------------------------------------------------
84// §2.3 TargetProvider Trait
85// ---------------------------------------------------------------------------
86
87/// Browser target manager trait. Implemented by the backend (e.g. bao_cdp)
88/// to provide target discovery/creation/closure.
89///
90/// Constraints: `Send + Sync`.
91pub trait TargetProvider: Send + Sync {
92    /// List all available browser targets.
93    fn list_targets(&self) -> Vec<TargetInfo>;
94
95    /// Create a new target (open a new page).
96    fn create_target(&self, url: &str) -> Result<TargetInfo, String>;
97
98    /// Close the specified target.
99    fn close_target(&self, target_id: &str) -> Result<(), String>;
100
101    /// Activate (bring to front) the specified target.
102    fn activate_target(&self, target_id: &str) -> Result<(), String>;
103}
104
105// ---------------------------------------------------------------------------
106// §8 ServerConfig Entity
107// ---------------------------------------------------------------------------
108
109/// CDP server configuration. Controls bind address, timeouts, concurrency
110/// limits and version strings.
111pub struct ServerConfig {
112    pub host: String,
113    pub port: u16,
114    pub http_timeout_seconds: u64,
115    pub max_sessions: usize,
116    pub browser_name: String,
117    pub protocol_version: String,
118    pub user_agent: Option<String>,
119    pub v8_version: Option<String>,
120    pub webkit_version: Option<String>,
121}
122
123impl Default for ServerConfig {
124    fn default() -> Self {
125        ServerConfig {
126            host: "127.0.0.1".into(),
127            port: 9222,
128            http_timeout_seconds: 30,
129            max_sessions: 100,
130            browser_name: "Bao/0.1.0".into(),
131            protocol_version: "1.3".into(),
132            user_agent: None,
133            v8_version: None,
134            webkit_version: None,
135        }
136    }
137}
138
139impl ServerConfig {
140    pub fn builder() -> ServerConfigBuilder {
141        ServerConfigBuilder::default()
142    }
143}
144
145#[derive(Default)]
146pub struct ServerConfigBuilder {
147    inner: ServerConfig,
148}
149
150impl ServerConfigBuilder {
151    pub fn host(mut self, host: impl Into<String>) -> Self {
152        self.inner.host = host.into();
153        self
154    }
155
156    pub fn port(mut self, port: u16) -> Self {
157        self.inner.port = port;
158        self
159    }
160
161    pub fn http_timeout_seconds(mut self, seconds: u64) -> Self {
162        self.inner.http_timeout_seconds = seconds;
163        self
164    }
165
166    pub fn max_sessions(mut self, max: usize) -> Self {
167        self.inner.max_sessions = max;
168        self
169    }
170
171    pub fn browser_name(mut self, name: impl Into<String>) -> Self {
172        self.inner.browser_name = name.into();
173        self
174    }
175
176    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
177        self.inner.user_agent = Some(ua.into());
178        self
179    }
180
181    pub fn v8_version(mut self, ver: impl Into<String>) -> Self {
182        self.inner.v8_version = Some(ver.into());
183        self
184    }
185
186    pub fn webkit_version(mut self, ver: impl Into<String>) -> Self {
187        self.inner.webkit_version = Some(ver.into());
188        self
189    }
190
191    pub fn build(self) -> ServerConfig {
192        self.inner
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    // -- ServerConfig defaults --
201
202    #[test]
203    fn server_config_default_host_is_127_0_0_1() {
204        assert_eq!(ServerConfig::default().host, "127.0.0.1");
205    }
206
207    #[test]
208    fn server_config_default_port_is_9222() {
209        assert_eq!(ServerConfig::default().port, 9222);
210    }
211
212    #[test]
213    fn server_config_default_timeout_is_30() {
214        assert_eq!(ServerConfig::default().http_timeout_seconds, 30);
215    }
216
217    #[test]
218    fn server_config_default_max_sessions_is_100() {
219        assert_eq!(ServerConfig::default().max_sessions, 100);
220    }
221
222    #[test]
223    fn server_config_default_browser_name_is_Bao() {
224        assert_eq!(ServerConfig::default().browser_name, "Bao/0.1.0");
225    }
226
227    #[test]
228    fn server_config_default_protocol_version_is_1_3() {
229        assert_eq!(ServerConfig::default().protocol_version, "1.3");
230    }
231
232    #[test]
233    fn server_config_default_user_agent_is_none() {
234        assert!(ServerConfig::default().user_agent.is_none());
235    }
236
237    #[test]
238    fn server_config_default_v8_version_is_none() {
239        assert!(ServerConfig::default().v8_version.is_none());
240    }
241
242    #[test]
243    fn server_config_default_webkit_version_is_none() {
244        assert!(ServerConfig::default().webkit_version.is_none());
245    }
246
247    // -- ServerConfigBuilder setters --
248
249    #[test]
250    fn builder_sets_host() {
251        assert_eq!(
252            ServerConfig::builder().host("0.0.0.0").build().host,
253            "0.0.0.0"
254        );
255    }
256
257    #[test]
258    fn builder_sets_port() {
259        assert_eq!(ServerConfig::builder().port(8080).build().port, 8080);
260    }
261
262    #[test]
263    fn builder_sets_timeout() {
264        assert_eq!(
265            ServerConfig::builder()
266                .http_timeout_seconds(60)
267                .build()
268                .http_timeout_seconds,
269            60
270        );
271    }
272
273    #[test]
274    fn builder_sets_max_sessions() {
275        assert_eq!(
276            ServerConfig::builder()
277                .max_sessions(50)
278                .build()
279                .max_sessions,
280            50
281        );
282    }
283
284    #[test]
285    fn builder_sets_browser_name() {
286        assert_eq!(
287            ServerConfig::builder()
288                .browser_name("Chrome/120")
289                .build()
290                .browser_name,
291            "Chrome/120"
292        );
293    }
294
295    #[test]
296    fn builder_sets_user_agent() {
297        let ua = ServerConfig::builder()
298            .user_agent("Mozilla/5.0")
299            .build()
300            .user_agent;
301        assert_eq!(ua.as_deref(), Some("Mozilla/5.0"));
302    }
303
304    #[test]
305    fn builder_sets_v8_version() {
306        let ver = ServerConfig::builder()
307            .v8_version("12.0")
308            .build()
309            .v8_version;
310        assert_eq!(ver.as_deref(), Some("12.0"));
311    }
312
313    #[test]
314    fn builder_sets_webkit_version() {
315        let ver = ServerConfig::builder()
316            .webkit_version("537.36")
317            .build()
318            .webkit_version;
319        assert_eq!(ver.as_deref(), Some("537.36"));
320    }
321
322    #[test]
323    fn builder_chaining_all_fields() {
324        let cfg = ServerConfig::builder()
325            .host("0.0.0.0")
326            .port(9223)
327            .http_timeout_seconds(120)
328            .max_sessions(200)
329            .browser_name("TestBrowser")
330            .user_agent("TestAgent")
331            .v8_version("13.0")
332            .webkit_version("600.0")
333            .build();
334        assert_eq!(cfg.host, "0.0.0.0");
335        assert_eq!(cfg.port, 9223);
336        assert_eq!(cfg.http_timeout_seconds, 120);
337        assert_eq!(cfg.max_sessions, 200);
338        assert_eq!(cfg.browser_name, "TestBrowser");
339        assert_eq!(cfg.user_agent.as_deref(), Some("TestAgent"));
340        assert_eq!(cfg.v8_version.as_deref(), Some("13.0"));
341        assert_eq!(cfg.webkit_version.as_deref(), Some("600.0"));
342    }
343
344    #[test]
345    fn builder_default_then_build_equals_default_config() {
346        let built = ServerConfig::builder().build();
347        let default = ServerConfig::default();
348        assert_eq!(built.host, default.host);
349        assert_eq!(built.port, default.port);
350        assert_eq!(built.http_timeout_seconds, default.http_timeout_seconds);
351        assert_eq!(built.max_sessions, default.max_sessions);
352        assert_eq!(built.browser_name, default.browser_name);
353        assert_eq!(built.protocol_version, default.protocol_version);
354        assert_eq!(built.user_agent, default.user_agent);
355        assert_eq!(built.v8_version, default.v8_version);
356        assert_eq!(built.webkit_version, default.webkit_version);
357    }
358}