use std::process::{Child, Command};
use std::{fs, thread, time};
use anyhow::{bail, Result};
use log::{debug, info, warn};
use portpicker;
use serde_json::json;
use crate::assets;
use crate::config::Config;
use crate::protocol::{DevtoolPage, EvaluateResponse};
use crate::websocket::WebSocket;
struct UserScript {
file_path: String,
content: String,
}
pub struct Injector {
config: Config,
port: u16,
}
impl Injector {
pub(crate) const INJECT_LOOP_SLEEP_MS: u64 = 1000;
pub(crate) const WAIT_DEBUGGING_PORT_TIMEOUT_MS: u64 = 30_000;
fn get_available_port(config: &Config) -> u16 {
if portpicker::is_free_tcp(config.port) {
info!("Using port: {}", config.port);
return config.port;
}
info!(
"Port {} is not available, finding another port",
config.port
);
let port = portpicker::pick_unused_port().expect("Port should be available");
info!("Found available port: {}", port);
port
}
pub fn new() -> Self {
let config = Config::parse_auto();
let port = Injector::get_available_port(&config);
Injector { config, port }
}
pub fn run(&self) -> Result<()> {
info!("Running injector");
debug!("{:#?}", self.config);
_ = self.spawn_process()?;
let prelude_script = self.get_prelude_script().unwrap_or(String::new());
let user_scripts = self.get_user_scripts();
let timeout_duration = time::Duration::from_millis(self.config.timeout);
let mut found_page_ids: Vec<String> = Vec::new();
let start_time = time::Instant::now();
loop {
let devtool_pages = self
.get_devtool_pages()
.expect("Should be able to get devtool pages");
debug!("{:#?}", devtool_pages);
for page in devtool_pages {
if found_page_ids.contains(&page.id) {
continue;
}
let mut ws = WebSocket::connect(&page.web_socket_debugger_url)
.expect("To connect to websocket");
if self.config.prelude {
info!("Injecting prelude script (id: {})", page.id);
self.evaluate(&mut ws, &prelude_script)
.expect("Should be able to evaluate JS");
}
for user_script in user_scripts.iter() {
info!("Injecting script: {}", user_script.file_path);
self.evaluate(&mut ws, &user_script.content)
.expect("Should be able to evaluate JS");
}
found_page_ids.push(page.id.clone());
}
let updated_devtool_pages = self
.get_devtool_pages()
.expect("Should be able to get devtool pages");
if found_page_ids.len() == updated_devtool_pages.len() {
info!("Stopping injection loop");
break;
}
if start_time.elapsed() >= timeout_duration {
bail!("Injection loop timed out");
}
thread::sleep(time::Duration::from_millis(Self::INJECT_LOOP_SLEEP_MS));
}
info!("Injection success");
Ok(())
}
fn get_devtool_pages(&self) -> Result<Vec<DevtoolPage>, reqwest::Error> {
let url = format!("http://{}:{}/json/list", &self.config.host, &self.port);
let client = reqwest::blocking::Client::new();
let response = client.get(url).send()?.error_for_status()?;
let pages_response = response.json::<Vec<DevtoolPage>>()?;
Ok(pages_response)
}
fn get_prelude_script(&self) -> Option<String> {
if !self.config.prelude {
return None;
}
let file = assets::JS::get("prelude.js").unwrap();
let script =
std::str::from_utf8(file.data.as_ref()).expect("Script should be a valid UTF-8 file");
Some(String::from(script))
}
fn get_user_scripts(&self) -> Vec<UserScript> {
let scripts: Vec<UserScript> = self
.config
.script
.iter()
.map(|s| {
let content =
fs::read_to_string(s).expect("Should have been able to read the file");
UserScript {
file_path: s.to_string(),
content,
}
})
.collect();
scripts
}
fn spawn_process(&self) -> Result<Child> {
let mut args = vec![format!("--remote-debugging-port={}", &self.port)];
args.extend(self.config.arg.iter().cloned());
debug!(
"Spawning electron app: {} (args: {:#?})",
&self.config.app, args
);
let child = Command::new(&self.config.app).args(args).spawn()?;
info!("Waiting for {}ms", self.config.delay);
thread::sleep(time::Duration::from_millis(self.config.delay));
let timeout_duration = time::Duration::from_millis(Self::WAIT_DEBUGGING_PORT_TIMEOUT_MS);
info!("Waiting for remote debugging port");
let start_time = time::Instant::now();
loop {
if self.get_devtool_pages().is_ok() {
info!("Connected to remote debugging port");
break;
}
if start_time.elapsed() >= timeout_duration {
bail!("Unable to connect to remote debugging port");
}
}
Ok(child)
}
fn evaluate(&self, ws: &mut WebSocket, expression: &str) -> Result<()> {
let payload = json!({
"id": 1,
"method": "Runtime.evaluate",
"params": {
"expression": expression,
"objectGroup": "inject",
"includeCommandLineAPI": true,
"silent": true,
"userGesture": true,
"awaitPromise": true,
},
});
let payload_json = serde_json::to_string(&payload)?;
let result_msg = ws.send_and_receive(&payload_json)?;
debug!("[Runtime.evaluate] Raw message: {:#?}", result_msg);
if !result_msg.is_text() {
warn!(
"[Runtime.evaluate] Unexpected result from WebSocket: {:#?}",
result_msg
);
return Ok(());
}
let result_json = result_msg.to_text()?;
let response: EvaluateResponse = serde_json::from_str(result_json)?;
debug!("[Runtime.evaluate] Parsed response: {:#?}", response);
if response.result.exception_details.is_some() {
warn!(
"[Runtime.evaluate] Caught exception while evaluating script: {:#?}",
response
);
return Ok(());
}
Ok(())
}
}