use std::{net::TcpStream, time::{Duration, Instant}};
use serde::Deserialize;
use serde_json::{json, Value};
use tungstenite::{client, WebSocket, error::Error, handshake::HandshakeError};
use url::Url;
pub type NetworkError = Error;
pub type MessageParameter = Value;
#[derive(Debug)]
pub enum ClientError {
CannotConnect,
InvalidTab
}
impl From<reqwest::Error> for ClientError {
fn from(_: reqwest::Error) -> Self {
ClientError::CannotConnect
}
}
#[derive(Debug)]
pub enum MessageError {
NetworkError(NetworkError),
InvalidRequest(Value),
InvalidResponse,
NoMessage
}
impl From<Error> for MessageError {
fn from(error: Error) -> Self {
match error {
Error::Utf8 => MessageError::InvalidResponse,
_ => MessageError::NetworkError(error),
}
}
}
#[allow(non_snake_case, dead_code)]
#[derive(Deserialize)]
pub struct Tab {
description: String,
devtoolsFrontendUrl: String,
id: String,
title: String,
r#type: String,
url: String,
webSocketDebuggerUrl: String
}
pub struct CdpClient {
host: String,
port: u16,
}
impl CdpClient {
pub fn new() -> Self {
Self::custom("localhost", 9222)
}
pub fn custom(host: &str, port: u16) -> Self {
Self { host: host.to_string(), port }
}
pub fn get_tabs(&self) -> Result<Vec<Tab>, ClientError> {
let tabs = reqwest::blocking::get(format!("http://{}:{}/json", self.host, self.port))?
.json::<Vec<Tab>>()?;
Ok(tabs)
}
pub fn connect_to_target(&self, target_id: &str) -> Result<CdpConnection, ClientError> {
let ws_url = format!("ws://{}:{}/devtools/page/{}", self.host, self.port, target_id);
CdpClient::make_connection(&ws_url, self.port)
}
pub fn connect_to_tab(&self, tab_index: usize) -> Result<CdpConnection, ClientError> {
let tabs = self.get_tabs()?;
let ws_url = match tabs.get(tab_index) {
Some(tab) => tab.webSocketDebuggerUrl.clone(),
None => return Err(ClientError::InvalidTab),
};
CdpClient::make_connection(&ws_url, self.port)
}
fn make_connection(url: &str, port: u16) -> Result<CdpConnection, ClientError> {
let url = Url::parse(&url).unwrap();
let mut addrs = url.socket_addrs(|| Some(port)).unwrap();
addrs.sort();
for addr in addrs {
if let Ok(stream) = TcpStream::connect(addr) {
stream.set_nonblocking(true).unwrap();
let mut result = client(url.clone(), stream);
loop {
match result {
Ok((socket, _)) => return Ok(CdpConnection::new(socket)),
Err(HandshakeError::Failure(_)) => return Err(ClientError::CannotConnect),
Err(HandshakeError::Interrupted(mid)) => result = mid.handshake(),
}
}
}
}
Err(ClientError::CannotConnect)
}
}
impl Default for CdpClient {
fn default() -> Self {
Self::new()
}
}
#[macro_export]
macro_rules! parms {
($($name:literal, $value:expr),*) => {{
vec![$(($name, cdp_rs::MessageParameter::from($value))),*]
}};
}
pub struct CdpConnection {
socket: WebSocket<TcpStream>,
message_id: i64,
}
impl CdpConnection {
fn new(socket: WebSocket<TcpStream>) -> Self {
Self { socket, message_id: 1 }
}
pub fn send(&mut self, method: &'static str, parms: Vec<(&'static str, MessageParameter)>) -> Result<Value, MessageError> {
let message_id = self.message_id;
let mut map = serde_json::Map::new();
for p in parms {
map.insert(p.0.to_string(), p.1);
}
let data = json!({
"id": self.message_id,
"method": method,
"params": map
});
self.message_id += 1;
self.socket.write_message(tungstenite::Message::Text(data.to_string()))?;
let result = self.wait_for(None, |m| {
(m.get("error").is_some() || m.get("result").is_some()) &&
m["id"].as_i64().unwrap() == message_id
});
if let Ok(r) = &result {
if r.get("error").is_some() { return Err(MessageError::InvalidRequest(r.clone())) }
}
result
}
pub fn wait_message(&mut self) -> Result<Value, MessageError> {
if let Ok(msg) = self.socket.read_message() {
let text = msg.into_text()?;
return match serde_json::from_str::<Value>(&text) {
Err(_) => Err(MessageError::InvalidResponse),
Ok(m) => Ok(m)
}
}
Err(MessageError::NoMessage)
}
pub fn wait_event(&mut self, event: &str, timeout: Option<Duration>) -> Result<Value, MessageError> {
self.wait_for(timeout, |m| {
if let Some(method) = m.get("method") {
if method == event {
return true
}
}
return false
})
}
pub fn wait_for<F>(&mut self, timeout: Option<Duration>, f: F) -> Result<Value, MessageError>
where F: Fn(&Value) -> bool {
let timeout = match timeout {
Some(t) => t,
None => Duration::from_secs(300),
};
let now = Instant::now();
while Instant::now() - now < timeout {
let m = self.wait_message();
match m {
Ok(m) => if f(&m) { return Ok(m) },
Err(MessageError::NoMessage) => {},
_ => { break; }
}
}
Err(MessageError::NoMessage)
}
}
impl Drop for CdpConnection {
fn drop(&mut self) {
if self.socket.close(None).is_ok() {
for _ in 0..100 {
if matches!(self.socket.read_message(), Err(Error::ConnectionClosed) | Err(Error::AlreadyClosed)) {
break;
}
}
}
}
}