1use std::collections::HashMap;
36use std::sync::atomic::{AtomicU64, Ordering};
37use std::sync::{Arc, Mutex, OnceLock};
38use std::time::Duration;
39
40use serde::{Deserialize, Serialize};
41use tokio::sync::{mpsc, oneshot};
42
43#[path = "chrome/pending.rs"]
44mod pending;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
48#[serde(rename_all = "snake_case")]
49pub enum ChromePermissionMode {
50 Observe,
52 #[default]
55 Assist,
56 Control,
59}
60
61impl ChromePermissionMode {
62 pub fn as_str(self) -> &'static str {
63 match self {
64 ChromePermissionMode::Observe => "observe",
65 ChromePermissionMode::Assist => "assist",
66 ChromePermissionMode::Control => "control",
67 }
68 }
69
70 pub fn parse(value: &str) -> Option<Self> {
71 match value.trim().to_ascii_lowercase().as_str() {
72 "observe" => Some(ChromePermissionMode::Observe),
73 "assist" => Some(ChromePermissionMode::Assist),
74 "control" => Some(ChromePermissionMode::Control),
75 _ => None,
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct ChromeBrowser {
84 pub id: String,
85 pub name: String,
86 pub kind: String,
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct ChromeTab {
94 pub id: i64,
95 #[serde(default)]
96 pub window_id: Option<i64>,
97 #[serde(default)]
98 pub title: Option<String>,
99 #[serde(default)]
100 pub url: Option<String>,
101 #[serde(default)]
102 pub fav_icon_url: Option<String>,
103 #[serde(default)]
104 pub active: bool,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
109#[serde(rename_all = "camelCase")]
110pub struct ChromeSitePermission {
111 pub origin: String,
112 #[serde(default)]
113 pub inspect: bool,
114 #[serde(default)]
115 pub interact: bool,
116 #[serde(default)]
117 pub eval: bool,
118 #[serde(default)]
119 pub debugger: bool,
120 #[serde(default)]
121 pub download: bool,
122 #[serde(default)]
123 pub upload: bool,
124 #[serde(default)]
125 pub recording: bool,
126 #[serde(default)]
127 pub schedule: bool,
128 #[serde(default)]
129 pub always_allow: bool,
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
134#[serde(rename_all = "camelCase")]
135pub struct ChromeStatus {
136 pub connected: bool,
138 pub client_count: usize,
140 pub enabled: bool,
142 pub capabilities: Vec<String>,
144 pub mode: ChromePermissionMode,
145 #[serde(default)]
146 pub active_tab: Option<ChromeTab>,
147 #[serde(default)]
148 pub browser: Option<ChromeBrowser>,
149 #[serde(default)]
150 pub last_error: Option<String>,
151 #[serde(default)]
152 pub remote_addr: Option<String>,
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158pub struct ChromeCommand {
159 pub kind: String,
160 #[serde(default)]
161 pub params: serde_json::Value,
162}
163
164impl ChromeCommand {
165 pub fn new(kind: impl Into<String>) -> Self {
166 Self {
167 kind: kind.into(),
168 params: serde_json::Value::Null,
169 }
170 }
171
172 pub fn with_params(kind: impl Into<String>, params: serde_json::Value) -> Self {
173 Self {
174 kind: kind.into(),
175 params,
176 }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum ChromeError {
183 NotConnected,
185 Disabled,
187 Rejected(String),
189 Timeout,
191 Disconnected,
193 Remote(String),
195}
196
197impl std::fmt::Display for ChromeError {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 ChromeError::NotConnected => write!(f, "no Chrome extension is connected"),
201 ChromeError::Disabled => write!(f, "Chrome tools are not enabled for this session"),
202 ChromeError::Rejected(reason) => write!(f, "browser action rejected: {reason}"),
203 ChromeError::Timeout => write!(f, "Chrome extension did not respond in time"),
204 ChromeError::Disconnected => {
205 write!(f, "Chrome extension disconnected before responding")
206 }
207 ChromeError::Remote(message) => write!(f, "Chrome extension error: {message}"),
208 }
209 }
210}
211
212impl std::error::Error for ChromeError {}
213
214pub const CHROME_DISPATCH_TIMEOUT: Duration = Duration::from_secs(30);
216
217#[async_trait::async_trait]
220pub trait ChromeController: Send + Sync + 'static {
221 fn status(&self) -> ChromeStatus;
223
224 fn is_enabled(&self) -> bool {
226 self.status().enabled
227 }
228
229 fn set_enabled(&self, enabled: bool);
231
232 fn set_mode(&self, mode: ChromePermissionMode);
234
235 async fn dispatch(&self, command: ChromeCommand) -> Result<serde_json::Value, ChromeError>;
237}
238
239pub struct ChromeClientRegistration {
242 pub client_id: u64,
243 pub commands: mpsc::UnboundedReceiver<serde_json::Value>,
244}
245
246struct ClientHandle {
247 commands: mpsc::UnboundedSender<serde_json::Value>,
248 capabilities: Vec<String>,
249 remote_addr: Option<String>,
250 active_tab: Option<ChromeTab>,
251 browser: Option<ChromeBrowser>,
252}
253
254#[derive(Default)]
255struct BridgeState {
256 clients: HashMap<u64, ClientHandle>,
257 pending: HashMap<String, oneshot::Sender<Result<serde_json::Value, String>>>,
258 enabled: bool,
259 mode: ChromePermissionMode,
260 last_error: Option<String>,
261}
262
263impl BridgeState {
264 fn new() -> Self {
265 Self {
266 clients: HashMap::new(),
267 pending: HashMap::new(),
268 enabled: true,
269 mode: ChromePermissionMode::default(),
270 last_error: None,
271 }
272 }
273}
274
275pub struct ChromeBridge {
286 state: Mutex<BridgeState>,
287 next_client_id: AtomicU64,
288 next_corr_id: AtomicU64,
289 dispatch_timeout: Duration,
290}
291
292impl Default for ChromeBridge {
293 fn default() -> Self {
294 Self {
295 state: Mutex::new(BridgeState::new()),
296 next_client_id: AtomicU64::new(1),
297 next_corr_id: AtomicU64::new(1),
298 dispatch_timeout: CHROME_DISPATCH_TIMEOUT,
299 }
300 }
301}
302
303impl ChromeBridge {
304 pub fn new() -> Self {
305 Self::default()
306 }
307
308 #[cfg(test)]
309 fn with_timeout(timeout: Duration) -> Self {
310 Self {
311 dispatch_timeout: timeout,
312 ..Self::default()
313 }
314 }
315
316 fn lock(&self) -> std::sync::MutexGuard<'_, BridgeState> {
317 self.state.lock().expect("chrome bridge mutex poisoned")
318 }
319
320 pub fn register_client(
323 &self,
324 remote_addr: Option<String>,
325 hello: &serde_json::Value,
326 ) -> ChromeClientRegistration {
327 let client_id = self.next_client_id.fetch_add(1, Ordering::SeqCst);
328 let (tx, rx) = mpsc::unbounded_channel();
329 let capabilities = string_array(hello.get("capabilities"));
330 let mut state = self.lock();
331 state.clients.insert(
332 client_id,
333 ClientHandle {
334 commands: tx,
335 capabilities,
336 remote_addr,
337 active_tab: None,
338 browser: None,
339 },
340 );
341 state.last_error = None;
342 ChromeClientRegistration {
343 client_id,
344 commands: rx,
345 }
346 }
347
348 pub fn unregister_client(&self, client_id: u64) {
350 let mut state = self.lock();
351 state.clients.remove(&client_id);
352 if state.clients.is_empty() {
353 for (_, tx) in state.pending.drain() {
355 let _ = tx.send(Err("extension disconnected".to_string()));
356 }
357 }
358 }
359
360 pub fn ingest_frame(&self, client_id: Option<u64>, frame: serde_json::Value) {
363 let kind = frame.get("type").and_then(|v| v.as_str()).unwrap_or("");
364 match kind {
365 "command/result" => {
366 let Some(id) = frame.get("id").and_then(|v| v.as_str()) else {
367 return;
368 };
369 let sender = {
370 let mut state = self.lock();
371 state.pending.remove(id)
372 };
373 if let Some(sender) = sender {
374 let ok = frame.get("ok").and_then(|v| v.as_bool()).unwrap_or(false);
375 let payload = if ok {
376 Ok(frame
377 .get("result")
378 .cloned()
379 .unwrap_or(serde_json::Value::Null))
380 } else {
381 Err(frame
382 .get("error")
383 .and_then(|v| v.as_str())
384 .unwrap_or("unknown extension error")
385 .to_string())
386 };
387 let _ = sender.send(payload);
388 }
389 }
390 "hello" => {
391 let caps = string_array(frame.get("capabilities"));
392 let mut state = self.lock();
393 if let Some(id) = client_id
394 && let Some(handle) = state.clients.get_mut(&id)
395 {
396 handle.capabilities = caps;
397 }
398 }
399 "state" => {
400 let tab = frame
401 .get("state")
402 .and_then(|s| s.get("activeTab"))
403 .and_then(|t| serde_json::from_value::<ChromeTab>(t.clone()).ok());
404 let mut state = self.lock();
405 if let Some(id) = client_id
406 && let Some(handle) = state.clients.get_mut(&id)
407 {
408 handle.active_tab = tab;
409 }
410 }
411 "tab/updated" => {
412 let tab = frame
413 .get("tab")
414 .and_then(|t| serde_json::from_value::<ChromeTab>(t.clone()).ok());
415 let mut state = self.lock();
416 if let Some(id) = client_id
417 && let Some(handle) = state.clients.get_mut(&id)
418 {
419 handle.active_tab = tab;
420 }
421 }
422 _ => {}
423 }
424 }
425
426 fn next_correlation_id(&self) -> String {
427 format!("rc-{}", self.next_corr_id.fetch_add(1, Ordering::SeqCst))
428 }
429}
430
431#[async_trait::async_trait]
432impl ChromeController for ChromeBridge {
433 fn status(&self) -> ChromeStatus {
434 let state = self.lock();
435 let primary = state.clients.values().next();
436 ChromeStatus {
437 connected: !state.clients.is_empty(),
438 client_count: state.clients.len(),
439 enabled: state.enabled,
440 capabilities: primary.map(|c| c.capabilities.clone()).unwrap_or_default(),
441 mode: state.mode,
442 active_tab: primary.and_then(|c| c.active_tab.clone()),
443 browser: primary.and_then(|c| c.browser.clone()),
444 last_error: state.last_error.clone(),
445 remote_addr: primary.and_then(|c| c.remote_addr.clone()),
446 }
447 }
448
449 fn set_enabled(&self, enabled: bool) {
450 self.lock().enabled = enabled;
451 }
452
453 fn set_mode(&self, mode: ChromePermissionMode) {
454 self.lock().mode = mode;
455 }
456
457 async fn dispatch(&self, command: ChromeCommand) -> Result<serde_json::Value, ChromeError> {
458 let corr = self.next_correlation_id();
459 let (res_tx, res_rx) = oneshot::channel();
460
461 let sender;
462 {
465 let mut state = self.lock();
466 if !state.enabled {
467 return Err(ChromeError::Disabled);
468 }
469 let Some(handle) = state.clients.values().next() else {
470 return Err(ChromeError::NotConnected);
471 };
472 let mut frame = serde_json::Map::new();
473 frame.insert("type".to_string(), serde_json::Value::String(command.kind));
474 frame.insert("id".to_string(), serde_json::Value::String(corr.clone()));
475 if let serde_json::Value::Object(params) = command.params {
476 for (key, value) in params {
477 if key != "type" && key != "id" {
478 frame.insert(key, value);
479 }
480 }
481 }
482 sender = handle.commands.clone();
483 if handle
484 .commands
485 .send(serde_json::Value::Object(frame))
486 .is_err()
487 {
488 return Err(ChromeError::Disconnected);
489 }
490 state.pending.insert(corr.clone(), res_tx);
491 }
492
493 let _pending = pending::Pending {
496 bridge: self,
497 corr: &corr,
498 sender,
499 };
500 match tokio::time::timeout(self.dispatch_timeout, res_rx).await {
501 Ok(Ok(Ok(result))) => Ok(result),
502 Ok(Ok(Err(message))) => Err(ChromeError::Remote(message)),
503 Ok(Err(_)) => Err(ChromeError::Disconnected),
504 Err(_) => Err(ChromeError::Timeout),
505 }
506 }
507}
508
509pub fn bridge() -> Arc<ChromeBridge> {
511 static BRIDGE: OnceLock<Arc<ChromeBridge>> = OnceLock::new();
512 BRIDGE.get_or_init(|| Arc::new(ChromeBridge::new())).clone()
513}
514
515fn string_array(value: Option<&serde_json::Value>) -> Vec<String> {
516 value
517 .and_then(|v| v.as_array())
518 .map(|items| {
519 items
520 .iter()
521 .filter_map(|item| item.as_str().map(ToString::to_string))
522 .collect()
523 })
524 .unwrap_or_default()
525}
526
527#[cfg(test)]
528#[path = "chrome/tests.rs"]
529mod tests;