use serde::{Deserialize, Serialize};
use std::{
fmt::Display,
fs::File,
io::{self, BufRead, BufReader},
};
use time::OffsetDateTime;
mod secret;
pub use secret::Secret;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CredentialHello {
pub v: Vec<u32>,
}
pub struct UnsupportedCredential;
impl Credential for UnsupportedCredential {
fn perform(
&self,
_registry: &RegistryInfo,
_action: &Action,
_args: &[&str],
) -> Result<CredentialResponse, Error> {
Err(Error::UrlNotSupported)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct CredentialRequest<'a> {
pub v: u32,
#[serde(borrow)]
pub registry: RegistryInfo<'a>,
#[serde(borrow, flatten)]
pub action: Action<'a>,
pub args: Vec<&'a str>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct RegistryInfo<'a> {
pub index_url: &'a str,
pub name: Option<&'a str>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub headers: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[non_exhaustive]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum Action<'a> {
#[serde(borrow)]
Get(Operation<'a>),
Login(LoginOptions<'a>),
Logout,
}
impl<'a> Display for Action<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Action::Get(_) => f.write_str("get"),
Action::Login(_) => f.write_str("login"),
Action::Logout => f.write_str("logout"),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct LoginOptions<'a> {
pub token: Option<Secret<&'a str>>,
pub login_url: Option<&'a str>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[non_exhaustive]
#[serde(tag = "operation", rename_all = "kebab-case")]
pub enum Operation<'a> {
Read,
Publish {
name: &'a str,
vers: &'a str,
cksum: &'a str,
},
Yank {
name: &'a str,
vers: &'a str,
},
Unyank {
name: &'a str,
vers: &'a str,
},
Owners {
name: &'a str,
},
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum CredentialResponse {
Get {
token: Secret<String>,
cache: CacheControl,
operation_independent: bool,
},
Login,
Logout,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum CacheControl {
Never,
Expires(#[serde(with = "time::serde::timestamp")] OffsetDateTime),
Session,
}
pub const PROTOCOL_VERSION_1: u32 = 1;
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "kebab-case", tag = "kind", content = "detail")]
#[non_exhaustive]
pub enum Error {
UrlNotSupported,
ProtocolNotSupported(u32),
Subprocess(String),
Io(String),
Serde(String),
Other(String),
OperationNotSupported,
NotFound,
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Error::Serde(err.to_string())
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err.to_string())
}
}
impl From<String> for Error {
fn from(err: String) -> Self {
Error::Other(err)
}
}
impl From<&str> for Error {
fn from(err: &str) -> Self {
Error::Other(err.to_string())
}
}
impl std::error::Error for Error {}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::UrlNotSupported => {
write!(f, "credential provider does not support this registry")
}
Error::ProtocolNotSupported(v) => write!(
f,
"credential provider does not support protocol version {v}"
),
Error::Io(msg) => write!(f, "i/o error: {msg}"),
Error::Serde(msg) => write!(f, "serialization error: {msg}"),
Error::Other(msg) => write!(f, "error: {msg}"),
Error::Subprocess(msg) => write!(f, "subprocess failed: {msg}"),
Error::OperationNotSupported => write!(
f,
"credential provider does not support the requested operation"
),
Error::NotFound => write!(f, "credential not found"),
}
}
}
pub trait Credential {
fn perform(
&self,
registry: &RegistryInfo,
action: &Action,
args: &[&str],
) -> Result<CredentialResponse, Error>;
}
pub fn main(credential: impl Credential) {
let result = doit(credential);
if result.is_err() {
serde_json::to_writer(std::io::stdout(), &result)
.expect("failed to serialize credential provider error");
println!();
}
}
fn doit(credential: impl Credential) -> Result<(), Error> {
let hello = CredentialHello {
v: vec![PROTOCOL_VERSION_1],
};
serde_json::to_writer(std::io::stdout(), &hello)?;
println!();
loop {
let mut buffer = String::new();
let len = std::io::stdin().read_line(&mut buffer)?;
if len == 0 {
return Ok(());
}
let request: CredentialRequest = serde_json::from_str(&buffer)?;
if request.v != PROTOCOL_VERSION_1 {
return Err(Error::ProtocolNotSupported(request.v));
}
serde_json::to_writer(
std::io::stdout(),
&credential.perform(&request.registry, &request.action, &request.args),
)?;
println!();
}
}
pub fn tty() -> Result<File, io::Error> {
#[cfg(unix)]
const IN_DEVICE: &str = "/dev/tty";
#[cfg(windows)]
const IN_DEVICE: &str = "CONIN$";
let stdin = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(IN_DEVICE)?;
Ok(stdin)
}
pub fn read_line() -> Result<String, io::Error> {
let mut reader = BufReader::new(tty()?);
let mut buf = String::new();
reader.read_line(&mut buf)?;
Ok(buf.trim().to_string())
}
pub fn read_token(
login_options: &LoginOptions,
registry: &RegistryInfo,
) -> Result<Secret<String>, Error> {
if let Some(token) = &login_options.token {
return Ok(token.to_owned());
}
if let Some(url) = login_options.login_url {
eprintln!("please paste the token found on {url} below");
} else if let Some(name) = registry.name {
eprintln!("please paste the token for {name} below");
} else {
eprintln!("please paste the token for {} below", registry.index_url);
}
Ok(Secret::from(read_line()?))
}