use std::process::Stdio;
use std::time::Duration;
use thiserror::Error;
pub(crate) const TIMEOUT_SECS: u64 = 30;
const MAX_OUTPUT_BYTES: usize = 64 * 1024;
#[derive(Debug, Error)]
pub enum KeyCommandError {
#[error("api_key_command is empty; it must name a program to run")]
NoProgram,
#[error("api_key_command `{program}` was not found on PATH")]
NotFound { program: String },
#[error("api_key_command `{program}` could not be started: {cause}")]
Spawn {
program: String,
cause: std::io::Error,
},
#[error("api_key_command `{program}` did not finish within {secs} seconds")]
Timeout { program: String, secs: u64 },
#[error("api_key_command `{program}` was killed by a signal")]
Signal { program: String },
#[error("api_key_command `{program}` exited {code}")]
Failed { program: String, code: i32 },
#[error(
"api_key_command `{program}` printed output that is not valid UTF-8; \
it must print the credential as text on stdout"
)]
NotUtf8 { program: String },
#[error(
"api_key_command `{program}` printed nothing; the whole trimmed stdout is \
the credential, so it must print one"
)]
Empty { program: String },
#[error(
"api_key_command `{program}` printed a value containing a character that \
cannot be sent in a header; it must print the credential alone"
)]
Unusable { program: String },
#[error(
"api_key_command `{program}` printed more than {limit} bytes; the whole \
trimmed stdout is the credential, so it must print one and nothing else"
)]
TooMuchOutput { program: String, limit: usize },
}
pub(crate) async fn run_bounded(argv: &[String]) -> Result<String, KeyCommandError> {
run(argv, Duration::from_secs(TIMEOUT_SECS)).await
}
pub(crate) async fn run(argv: &[String], timeout: Duration) -> Result<String, KeyCommandError> {
let Some((program, arguments)) = argv.split_first() else {
return Err(KeyCommandError::NoProgram);
};
let mut command = tokio::process::Command::new(program);
command
.args(arguments)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true);
let mut child = command.spawn().map_err(|cause| match cause.kind() {
std::io::ErrorKind::NotFound => KeyCommandError::NotFound {
program: program.clone(),
},
_ => KeyCommandError::Spawn {
program: program.clone(),
cause,
},
})?;
let mut stdout = child
.stdout
.take()
.expect("stdout is piped by the builder above");
let read_then_wait = async {
use tokio::io::AsyncReadExt;
let mut captured = Vec::new();
let mut buffer = [0u8; 8192];
let status = loop {
tokio::select! {
biased;
read = stdout.read(&mut buffer) => {
let read = read.map_err(|cause| KeyCommandError::Spawn {
program: program.clone(),
cause,
})?;
if read == 0 {
break child.wait().await.map_err(|cause| KeyCommandError::Spawn {
program: program.clone(),
cause,
})?;
}
append_output(program, &mut captured, &buffer[..read])?;
}
waited = child.wait() => {
let status = waited.map_err(|cause| KeyCommandError::Spawn {
program: program.clone(),
cause,
})?;
let final_drain = async {
loop {
let read = stdout.read(&mut buffer).await.map_err(|cause| {
KeyCommandError::Spawn {
program: program.clone(),
cause,
}
})?;
if read == 0 {
break;
}
append_output(program, &mut captured, &buffer[..read])?;
}
Ok(())
};
if let Ok(result) = tokio::time::timeout(
Duration::from_millis(10),
final_drain,
)
.await
{
result?;
}
break status;
}
}
};
Ok((captured, status))
};
let (captured, status) = match tokio::time::timeout(timeout, read_then_wait).await {
Ok(result) => result?,
Err(_) => {
return Err(KeyCommandError::Timeout {
program: program.clone(),
secs: timeout.as_secs(),
});
}
};
if !status.success() {
return Err(match status.code() {
Some(code) => KeyCommandError::Failed {
program: program.clone(),
code,
},
None => KeyCommandError::Signal {
program: program.clone(),
},
});
}
let printed = String::from_utf8(captured).map_err(|_| KeyCommandError::NotUtf8 {
program: program.clone(),
})?;
crate::auth::vet(&printed).map_err(|defect| match defect {
crate::auth::CredentialDefect::Empty => KeyCommandError::Empty {
program: program.clone(),
},
crate::auth::CredentialDefect::Unusable => KeyCommandError::Unusable {
program: program.clone(),
},
})
}
fn append_output(
program: &str,
captured: &mut Vec<u8>,
bytes: &[u8],
) -> Result<(), KeyCommandError> {
if captured.len().saturating_add(bytes.len()) > MAX_OUTPUT_BYTES {
return Err(KeyCommandError::TooMuchOutput {
program: program.to_owned(),
limit: MAX_OUTPUT_BYTES,
});
}
captured.extend_from_slice(bytes);
Ok(())
}