1use std::collections::{HashMap, VecDeque};
29use std::sync::Arc;
30use std::sync::atomic::{AtomicBool, Ordering};
31use std::time::Duration;
32
33use serde_json::Value;
34use tokio::sync::Mutex;
35use tokio::time::Instant;
36
37use crate::browser::tab::Tab;
38use crate::protocol::Event;
39use crate::util::base64_decode;
40use crate::{Error, Result};
41
42const MAX_BUFFERED: usize = 2000;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum WsDirection {
48 Sent,
50 Received,
52}
53
54impl WsDirection {
55 pub fn as_str(self) -> &'static str {
56 match self {
57 WsDirection::Sent => "sent",
58 WsDirection::Received => "received",
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct WsMessage {
66 pub direction: WsDirection,
68 pub url: String,
70 pub socket_id: String,
72 pub frame_id: String,
74 pub wsid: String,
76 pub opcode: i64,
78 pub data: String,
80}
81
82impl WsMessage {
83 pub fn is_text(&self) -> bool {
85 self.opcode == 1
86 }
87
88 pub fn is_binary(&self) -> bool {
90 self.opcode == 2
91 }
92
93 pub fn is_control(&self) -> bool {
95 matches!(self.opcode, 8..=10)
96 }
97
98 pub fn opcode_name(&self) -> String {
100 match self.opcode {
101 0 => "continuation".into(),
102 1 => "text".into(),
103 2 => "binary".into(),
104 8 => "close".into(),
105 9 => "ping".into(),
106 10 => "pong".into(),
107 n => format!("opcode({n})"),
108 }
109 }
110
111 pub fn text(&self) -> Option<String> {
113 self.is_text().then(|| self.data.clone())
114 }
115
116 pub fn bytes(&self) -> Vec<u8> {
118 if self.is_text() {
119 self.data.clone().into_bytes()
120 } else {
121 base64_decode(&self.data).unwrap_or_default()
122 }
123 }
124
125 pub fn text_lossy(&self) -> String {
127 if self.is_text() {
128 self.data.clone()
129 } else {
130 String::from_utf8_lossy(&self.bytes()).into_owned()
131 }
132 }
133
134 pub fn json(&self) -> Option<Value> {
136 if self.is_text() {
137 serde_json::from_str(&self.data).ok()
138 } else {
139 serde_json::from_slice(&self.bytes()).ok()
140 }
141 }
142}
143
144#[derive(Debug, Clone, Default)]
146pub struct WsSocket {
147 pub socket_id: String,
149 pub url: String,
151 pub opened: bool,
153 pub closed: bool,
155 pub error: String,
157}
158
159#[derive(Debug, Clone, Default)]
161pub struct WsFilter {
162 pub url_keywords: Vec<String>,
164 pub direction: Option<WsDirection>,
166 pub include_control: bool,
168}
169
170impl WsFilter {
171 pub fn new() -> Self {
172 Self::default()
173 }
174
175 pub fn url_contains(mut self, needle: &str) -> Self {
177 self.url_keywords.push(needle.to_string());
178 self
179 }
180
181 pub fn sent_only(mut self) -> Self {
183 self.direction = Some(WsDirection::Sent);
184 self
185 }
186
187 pub fn received_only(mut self) -> Self {
189 self.direction = Some(WsDirection::Received);
190 self
191 }
192
193 pub fn with_control(mut self) -> Self {
195 self.include_control = true;
196 self
197 }
198
199 fn url_matches(&self, url: &str) -> bool {
200 self.url_keywords.is_empty() || self.url_keywords.iter().any(|k| url.contains(k))
201 }
202
203 fn matches(&self, direction: WsDirection, opcode: i64, url: &str) -> bool {
204 if let Some(d) = self.direction
205 && d != direction
206 {
207 return false;
208 }
209 if !self.include_control && matches!(opcode, 8..=10) {
210 return false;
211 }
212 self.url_matches(url)
213 }
214}
215
216pub(crate) struct WsShared {
218 pub buf: Mutex<VecDeque<WsMessage>>,
219 pub sockets: Mutex<HashMap<String, WsSocket>>,
220 pub active: AtomicBool,
221}
222
223impl WsShared {
224 pub(crate) fn new() -> Self {
225 Self {
226 buf: Mutex::new(VecDeque::new()),
227 sockets: Mutex::new(HashMap::new()),
228 active: AtomicBool::new(false),
229 }
230 }
231}
232
233pub struct WsListener {
238 tab: Tab,
239}
240
241impl WsListener {
242 pub(crate) fn new(tab: Tab) -> Self {
243 Self { tab }
244 }
245
246 pub async fn start(&self) -> Result<()> {
248 self.start_with(WsFilter::default()).await
249 }
250
251 pub async fn start_with(&self, filter: WsFilter) -> Result<()> {
253 let shared = self.tab.core.ws.clone();
254 if shared.active.swap(true, Ordering::SeqCst) {
255 return Ok(()); }
257 shared.buf.lock().await.clear();
258 shared.sockets.lock().await.clear();
259
260 let events = self.tab.core.conn.subscribe();
261 let session = self.tab.core.session_id.clone();
262 let task = tokio::spawn(ws_loop(events, session, shared, filter));
263 *self.tab.core.ws_task.lock().await = Some(task);
264 Ok(())
265 }
266
267 pub fn listening(&self) -> bool {
269 self.tab.core.ws.active.load(Ordering::SeqCst)
270 }
271
272 pub async fn wait(&self, timeout: Option<Duration>) -> Result<Option<WsMessage>> {
274 let shared = &self.tab.core.ws;
275 if !shared.active.load(Ordering::SeqCst) {
276 return Err(Error::Other("尚未调用 websocket().start()".into()));
277 }
278 let deadline = timeout.map(|d| Instant::now() + d);
279 loop {
280 if let Some(m) = shared.buf.lock().await.pop_front() {
281 return Ok(Some(m));
282 }
283 if !shared.active.load(Ordering::SeqCst) {
284 return Ok(None); }
286 if let Some(dl) = deadline
287 && Instant::now() >= dl
288 {
289 return Ok(None);
290 }
291 tokio::time::sleep(Duration::from_millis(50)).await;
292 }
293 }
294
295 pub async fn wait_count(
297 &self,
298 count: usize,
299 timeout: Option<Duration>,
300 ) -> Result<Vec<WsMessage>> {
301 let total = timeout.unwrap_or_else(|| self.tab.core.timeout());
302 let deadline = Instant::now() + total;
303 let mut out = Vec::with_capacity(count);
304 while out.len() < count {
305 let remain = deadline.saturating_duration_since(Instant::now());
306 if remain.is_zero() {
307 break;
308 }
309 match self.wait(Some(remain)).await? {
310 Some(m) => out.push(m),
311 None => break,
312 }
313 }
314 Ok(out)
315 }
316
317 pub async fn messages(&self) -> Vec<WsMessage> {
319 self.tab.core.ws.buf.lock().await.drain(..).collect()
320 }
321
322 pub async fn sockets(&self) -> Vec<WsSocket> {
324 self.tab
325 .core
326 .ws
327 .sockets
328 .lock()
329 .await
330 .values()
331 .cloned()
332 .collect()
333 }
334
335 pub async fn clear(&self) {
337 self.tab.core.ws.buf.lock().await.clear();
338 }
339
340 pub fn steps(&self) -> WsSteps {
342 WsSteps {
343 tab: self.tab.clone(),
344 }
345 }
346
347 pub async fn stop(&self) -> Result<()> {
349 self.tab.core.ws.active.store(false, Ordering::SeqCst);
350 if let Some(h) = self.tab.core.ws_task.lock().await.take() {
351 h.abort();
352 }
353 self.tab.core.ws.buf.lock().await.clear();
354 self.tab.core.ws.sockets.lock().await.clear();
355 Ok(())
356 }
357}
358
359pub struct WsSteps {
361 tab: Tab,
362}
363
364impl WsSteps {
365 pub async fn next(&self, timeout: Option<Duration>) -> Result<Option<WsMessage>> {
367 WsListener::new(self.tab.clone()).wait(timeout).await
368 }
369}
370
371async fn ws_loop(
373 mut events: tokio::sync::broadcast::Receiver<Event>,
374 session: String,
375 shared: Arc<WsShared>,
376 filter: WsFilter,
377) {
378 loop {
379 let ev = match events.recv().await {
380 Ok(ev) => ev,
381 Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
382 tracing::warn!(skipped = n, "WebSocket 监听落后,跳过部分事件");
383 continue;
384 }
385 Err(_) => break,
386 };
387 if !shared.active.load(Ordering::SeqCst) {
388 break;
389 }
390 if ev.session_id.as_deref() != Some(&session) {
391 continue;
392 }
393 match ev.method.as_str() {
394 "Page.webSocketCreated" => {
395 let id = socket_id(&ev.params);
396 let url = ev.params["requestURL"]
397 .as_str()
398 .unwrap_or_default()
399 .to_string();
400 let mut socks = shared.sockets.lock().await;
401 let s = socks.entry(id.clone()).or_default();
402 s.socket_id = id;
403 if !url.is_empty() {
404 s.url = url;
405 }
406 }
407 "Page.webSocketOpened" => {
408 let id = socket_id(&ev.params);
409 let url = ev.params["effectiveURL"]
410 .as_str()
411 .unwrap_or_default()
412 .to_string();
413 let mut socks = shared.sockets.lock().await;
414 let s = socks.entry(id.clone()).or_default();
415 s.socket_id = id;
416 s.opened = true;
417 if !url.is_empty() {
418 s.url = url;
419 }
420 }
421 "Page.webSocketClosed" => {
422 let id = socket_id(&ev.params);
423 let err = ev.params["error"].as_str().unwrap_or_default().to_string();
424 let mut socks = shared.sockets.lock().await;
425 let s = socks.entry(id.clone()).or_default();
426 s.socket_id = id;
427 s.closed = true;
428 s.error = err;
429 }
430 "Page.webSocketFrameSent" => {
431 push_frame(&shared, &filter, WsDirection::Sent, &ev.params).await;
432 }
433 "Page.webSocketFrameReceived" => {
434 push_frame(&shared, &filter, WsDirection::Received, &ev.params).await;
435 }
436 _ => {}
437 }
438 }
439 tracing::debug!(%session, "WebSocket 监听任务结束");
440}
441
442async fn push_frame(
444 shared: &Arc<WsShared>,
445 filter: &WsFilter,
446 direction: WsDirection,
447 params: &Value,
448) {
449 let id = socket_id(params);
450 let opcode = params["opcode"].as_i64().unwrap_or(-1);
451 let url = shared
452 .sockets
453 .lock()
454 .await
455 .get(&id)
456 .map(|s| s.url.clone())
457 .unwrap_or_default();
458 if !filter.matches(direction, opcode, &url) {
459 return;
460 }
461 let msg = WsMessage {
462 direction,
463 url,
464 socket_id: id,
465 frame_id: params["frameId"].as_str().unwrap_or_default().to_string(),
466 wsid: params["wsid"].as_str().unwrap_or_default().to_string(),
467 opcode,
468 data: params["data"].as_str().unwrap_or_default().to_string(),
469 };
470 let mut buf = shared.buf.lock().await;
471 if buf.len() >= MAX_BUFFERED {
472 buf.pop_front();
473 }
474 buf.push_back(msg);
475}
476
477fn socket_id(params: &Value) -> String {
479 let frame = params["frameId"].as_str().unwrap_or_default();
480 let wsid = params["wsid"].as_str().unwrap_or_default();
481 format!("{frame}---{wsid}")
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use serde_json::json;
488
489 #[test]
490 fn text_frame_helpers() {
491 let m = WsMessage {
492 direction: WsDirection::Received,
493 url: "wss://x/path".into(),
494 socket_id: "f---1".into(),
495 frame_id: "f".into(),
496 wsid: "1".into(),
497 opcode: 1,
498 data: r#"{"a":1,"b":[2,3]}"#.into(),
499 };
500 assert!(m.is_text());
501 assert!(!m.is_binary());
502 assert!(!m.is_control());
503 assert_eq!(m.opcode_name(), "text");
504 assert_eq!(m.text().as_deref(), Some(r#"{"a":1,"b":[2,3]}"#));
505 assert_eq!(m.bytes(), br#"{"a":1,"b":[2,3]}"#.to_vec());
506 let j = m.json().unwrap();
507 assert_eq!(j["b"][1], 3);
508 }
509
510 #[test]
511 fn binary_frame_is_base64() {
512 let m = WsMessage {
514 direction: WsDirection::Sent,
515 url: String::new(),
516 socket_id: "f---2".into(),
517 frame_id: "f".into(),
518 wsid: "2".into(),
519 opcode: 2,
520 data: "AQIDBA==".into(),
521 };
522 assert!(m.is_binary());
523 assert!(m.text().is_none());
524 assert_eq!(m.bytes(), vec![1, 2, 3, 4]);
525 assert_eq!(m.opcode_name(), "binary");
526 }
527
528 #[test]
529 fn binary_json_decodes_then_parses() {
530 let m = WsMessage {
532 direction: WsDirection::Received,
533 url: String::new(),
534 socket_id: "f---3".into(),
535 frame_id: "f".into(),
536 wsid: "3".into(),
537 opcode: 2,
538 data: crate::util::base64_encode(br#"{"k":42}"#),
539 };
540 assert_eq!(m.json().unwrap()["k"], 42);
541 assert_eq!(m.text_lossy(), r#"{"k":42}"#);
542 }
543
544 #[test]
545 fn filter_direction_and_control_and_url() {
546 let f = WsFilter::default();
548 assert!(f.matches(WsDirection::Sent, 1, "wss://a"));
549 assert!(f.matches(WsDirection::Received, 2, "wss://a"));
550 assert!(!f.matches(WsDirection::Sent, 9, "wss://a")); let f = WsFilter::new().with_control();
554 assert!(f.matches(WsDirection::Sent, 9, "wss://a"));
555
556 let f = WsFilter::new().sent_only();
558 assert!(f.matches(WsDirection::Sent, 1, "x"));
559 assert!(!f.matches(WsDirection::Received, 1, "x"));
560
561 let f = WsFilter::new().url_contains("/live/");
563 assert!(f.matches(WsDirection::Sent, 1, "wss://h/live/room"));
564 assert!(!f.matches(WsDirection::Sent, 1, "wss://h/other"));
565 }
566
567 #[test]
568 fn socket_id_combines_frame_and_wsid() {
569 assert_eq!(socket_id(&json!({"frameId":"abc","wsid":"7"})), "abc---7");
570 }
571}