use std::io::{BufRead, Write};
use anyhow::Context;
use zygo_core::image::auth::{Credential, CredentialStore};
use zygo_core::image::{RegistryClient, Store, Verified};
use crate::cli::Cli;
use crate::output::Style;
pub fn run(
cli: &Cli,
registry: &str,
username: Option<&str>,
password_stdin: bool,
) -> anyhow::Result<u8> {
let paths = super::paths(cli);
paths.ensure()?;
let style = Style::stdout();
let username = match username {
Some(u) => u.to_string(),
None => prompt_line(&format!("Username for {registry}: "))?,
};
anyhow::ensure!(!username.is_empty(), "a username is required");
let password = if password_stdin {
read_all_stdin()?
} else {
prompt_password(
registry,
&format!("Password for {username} at {registry}: "),
)?
};
anyhow::ensure!(!password.is_empty(), "a password is required");
let credential = Credential {
username: username.clone(),
password,
};
let client = RegistryClient::new(Store::new(paths.clone()))?;
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
let verdict = runtime
.block_on(client.verify(registry, &credential))
.with_context(|| format!("`{registry}` did not accept that credential"))?;
if verdict == Verified::NoCredentialsNeeded {
println!(
"{} {registry} serves anyone; nothing was stored",
style.yellow("·")
);
return Ok(0);
}
let path = CredentialStore::zygo_path(&paths);
let mut store = CredentialStore::from_file(&path);
store.insert(registry, credential);
store
.save(&path)
.with_context(|| format!("could not write {}", path.display()))?;
println!("{} signed in to {registry} as {username}", style.green("✓"));
println!("{}", style.dim(&format!(" stored in {}", path.display())));
Ok(0)
}
fn prompt_line(prompt: &str) -> anyhow::Result<String> {
print!("{prompt}");
std::io::stdout().flush()?;
let mut line = String::new();
std::io::stdin()
.lock()
.read_line(&mut line)
.context("could not read from the terminal")?;
Ok(line.trim_end_matches(['\n', '\r']).to_string())
}
pub(super) fn read_all_stdin() -> anyhow::Result<String> {
let mut buf = String::new();
std::io::Read::read_to_string(&mut std::io::stdin().lock(), &mut buf)
.context("could not read the password from standard input")?;
Ok(buf.trim_end_matches(['\n', '\r']).to_string())
}
#[cfg(unix)]
pub(super) fn prompt_password(registry: &str, prompt: &str) -> anyhow::Result<String> {
use std::os::fd::AsRawFd;
let fd = std::io::stdin().as_raw_fd();
if unsafe { libc::isatty(fd) } != 1 {
anyhow::bail!(
"standard input is not a terminal, so the password cannot be read without \
echoing it\n → pipe it instead: printf %s \"$TOKEN\" | zygo login \
{registry} --password-stdin"
);
}
let mut original: libc::termios = unsafe { core::mem::zeroed() };
if unsafe { libc::tcgetattr(fd, &mut original) } != 0 {
return Err(std::io::Error::last_os_error()).context("could not read the terminal mode");
}
let mut quiet = original;
quiet.c_lflag &= !libc::ECHO;
if unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &quiet) } != 0 {
return Err(std::io::Error::last_os_error()).context("could not turn the echo off");
}
let restore = EchoRestored { fd };
install_echo_handler(fd);
let typed = prompt_line(prompt);
drop(restore);
println!();
typed
}
#[cfg(unix)]
struct EchoRestored {
fd: std::os::fd::RawFd,
}
#[cfg(unix)]
impl Drop for EchoRestored {
fn drop(&mut self) {
restore_echo(self.fd);
}
}
#[cfg(unix)]
fn restore_echo(fd: std::os::fd::RawFd) {
unsafe {
let mut current: libc::termios = core::mem::zeroed();
if libc::tcgetattr(fd, &mut current) == 0 {
current.c_lflag |= libc::ECHO;
libc::tcsetattr(fd, libc::TCSAFLUSH, ¤t);
}
}
}
#[cfg(unix)]
static PROMPTING: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
#[cfg(unix)]
fn install_echo_handler(fd: std::os::fd::RawFd) {
use std::sync::atomic::Ordering;
PROMPTING.store(fd, Ordering::SeqCst);
extern "C" fn on_interrupt(signal: libc::c_int) {
use std::sync::atomic::Ordering;
let fd = PROMPTING.load(Ordering::SeqCst);
if fd >= 0 {
restore_echo(fd);
}
unsafe {
libc::signal(signal, libc::SIG_DFL);
libc::raise(signal);
}
}
unsafe {
let handler = on_interrupt as *const () as libc::sighandler_t;
libc::signal(libc::SIGINT, handler);
libc::signal(libc::SIGTERM, handler);
}
}
#[cfg(not(unix))]
pub(super) fn prompt_password(_registry: &str, _prompt: &str) -> anyhow::Result<String> {
anyhow::bail!("reading a password without echo needs a Unix terminal; use --password-stdin")
}