use keyhunter::{ApiKeyCollector, ApiKeyMessage, WebsiteWalkBuilder};
use miette::{miette, IntoDiagnostic as _, Result};
use std::{
env, ops,
path::{Path, PathBuf},
process::{self, Child, Stdio},
sync::mpsc,
thread,
time::Duration,
};
#[cfg(not(tarpaulin_include))]
fn root() -> PathBuf {
PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
.canonicalize()
.unwrap()
}
#[cfg(not(tarpaulin_include))]
fn setup_sb_admin() -> Result<PathBuf> {
use std::process::Command;
let root = root();
let setup_script = root.join("tasks/sb_admin.sh");
assert!(setup_script.is_file());
let site_dir = String::from_utf8(
Command::new(setup_script)
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn()
.into_diagnostic()?
.wait_with_output()
.into_diagnostic()?
.stdout,
)
.into_diagnostic()?;
let site_dir = PathBuf::from(site_dir.trim());
assert!(site_dir.is_dir());
Ok(site_dir)
}
fn serve_local(site_dir: &Path) -> Result<AutoKilledChild> {
let mut serve = process::Command::new("npx");
serve
.args(["http-server", "-p", "8080"])
.arg(site_dir)
.stdout(Stdio::null());
serve.spawn().into_diagnostic().map(AutoKilledChild::from)
}
fn poll_server(site_url: &str) -> Result<()> {
const MAX_ATTEMPTS: u32 = 6;
let mut i = MAX_ATTEMPTS;
loop {
if i == 0 {
return Err(miette!(
"Server at '{}' was not ready after '{}' attempts",
site_url,
MAX_ATTEMPTS
));
}
if ureq::get(site_url)
.timeout(Duration::from_millis(500))
.call()
.is_ok()
{
return Ok(());
}
i -= 1;
println!("site is not ready, retrying in 1 second...");
thread::sleep(Duration::from_secs(1));
}
}
#[derive(Debug)]
struct AutoKilledChild(Child);
impl ops::Deref for AutoKilledChild {
type Target = Child;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ops::DerefMut for AutoKilledChild {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Child> for AutoKilledChild {
fn from(child: Child) -> Self {
Self(child)
}
}
impl Drop for AutoKilledChild {
fn drop(&mut self) {
self.0.kill().unwrap()
}
}
#[test]
fn test_sb_admin() -> Result<()> {
miette::set_hook(Box::new(|_| {
Box::new(
miette::MietteHandlerOpts::new()
.graphical_theme(miette::GraphicalTheme::default())
.terminal_links(true)
.unicode(true)
.context_lines(3)
.width(180)
.color(true)
.with_cause_chain()
.build(),
)
}))
.unwrap();
let site_dir = setup_sb_admin()?;
println!("starting server");
let mut serve_child = serve_local(&site_dir)?;
const SITE_URL: &str = "http://localhost:8080";
println!("waiting for server to be ready");
if let Err(e) = poll_server(SITE_URL) {
drop(serve_child);
panic!("{e}");
}
let builder = WebsiteWalkBuilder::new()
.with_timeout(Duration::from_secs(1))
.with_shared_cache(false);
let scripts_res = builder.collect(SITE_URL);
let (key_sender, key_receiver) = mpsc::channel();
let (script_sender, script_receiver) = mpsc::channel();
let key_handle = thread::spawn(move || {
let mut keys = vec![];
while let Ok(message) = key_receiver.recv() {
match message {
ApiKeyMessage::Stop => break,
ApiKeyMessage::Keys(api_keys) => {
keys.extend(api_keys);
}
ApiKeyMessage::RecoverableFailure(err) => {
println!("{:?}", err);
}
_ => {}
}
}
keys
});
let collector =
ApiKeyCollector::new(Default::default(), script_receiver, key_sender).with_random_ua(true);
let collector_handle = thread::spawn(move || {
collector.collect();
});
let walker = builder.build(script_sender);
let walk_res = walker.walk(SITE_URL);
let collector_handle_result = collector_handle.join();
let key_handle_result = key_handle.join();
serve_child.kill().into_diagnostic()?;
let mut scripts = scripts_res?;
collector_handle_result.unwrap();
let api_keys = key_handle_result.unwrap();
walk_res?;
println!("Found {} scripts:\n{:#?}", scripts.len(), scripts);
assert_eq!(scripts.len(), 11);
scripts.sort_unstable();
scripts.dedup();
assert_eq!(scripts.len(), 11);
assert!(api_keys.is_empty());
Ok(())
}