#![doc = include_str!("../examples/file-provider.rs")]
use serde::{Deserialize, Serialize};
use std::{fmt::Display, io};
use time::OffsetDateTime;
mod error;
mod secret;
mod stdio;
pub use error::Error;
pub use secret::Secret;
use stdio::stdin_stdout_to_console;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
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, PartialEq, Eq)]
#[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>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub args: Vec<&'a str>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct RegistryInfo<'a> {
pub index_url: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<&'a str>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub headers: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum Action<'a> {
#[serde(borrow)]
Get(Operation<'a>),
Login(LoginOptions<'a>),
Logout,
#[serde(other)]
Unknown,
}
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"),
Action::Unknown => f.write_str("<unknown>"),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct LoginOptions<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<Secret<&'a str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub login_url: Option<&'a str>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[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,
},
#[serde(other)]
Unknown,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum CredentialResponse {
Get {
token: Secret<String>,
#[serde(flatten)]
cache: CacheControl,
operation_independent: bool,
},
Login,
Logout,
#[serde(other)]
Unknown,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "cache", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum CacheControl {
Never,
Expires {
#[serde(with = "time::serde::timestamp")]
expiration: OffsetDateTime,
},
Session,
#[serde(other)]
Unknown,
}
pub const PROTOCOL_VERSION_1: u32 = 1;
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).map_err(|e| Error::Other(e));
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<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
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 = deserialize_request(&buffer)?;
let response = stdin_stdout_to_console(|| {
credential.perform(&request.registry, &request.action, &request.args)
})?;
serde_json::to_writer(std::io::stdout(), &response)?;
println!();
}
}
fn deserialize_request(
value: &str,
) -> Result<CredentialRequest<'_>, Box<dyn std::error::Error + Send + Sync>> {
let request: CredentialRequest<'_> = serde_json::from_str(&value)?;
if request.v != PROTOCOL_VERSION_1 {
return Err(format!("unsupported protocol version {}", request.v).into());
}
Ok(request)
}
pub fn read_line() -> Result<String, io::Error> {
let mut buf = String::new();
io::stdin().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().map_err(Box::new)?))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsupported_version() {
let msg = r#"{"v":999, "registry": {"index-url":""}, "args":[], "kind": "unexpected"}"#;
assert_eq!(
"unsupported protocol version 999",
deserialize_request(msg).unwrap_err().to_string()
);
}
#[test]
fn cache_control() {
let cc = CacheControl::Expires {
expiration: OffsetDateTime::from_unix_timestamp(1693928537).unwrap(),
};
let json = serde_json::to_string(&cc).unwrap();
assert_eq!(json, r#"{"cache":"expires","expiration":1693928537}"#);
let cc = CacheControl::Session;
let json = serde_json::to_string(&cc).unwrap();
assert_eq!(json, r#"{"cache":"session"}"#);
let cc: CacheControl = serde_json::from_str(r#"{"cache":"unknown-kind"}"#).unwrap();
assert_eq!(cc, CacheControl::Unknown);
assert_eq!(
"missing field `expiration`",
serde_json::from_str::<CacheControl>(r#"{"cache":"expires"}"#)
.unwrap_err()
.to_string()
);
}
#[test]
fn credential_response() {
let cr = CredentialResponse::Get {
cache: CacheControl::Never,
operation_independent: true,
token: Secret::from("value".to_string()),
};
let json = serde_json::to_string(&cr).unwrap();
assert_eq!(
json,
r#"{"kind":"get","token":"value","cache":"never","operation_independent":true}"#
);
let cr = CredentialResponse::Login;
let json = serde_json::to_string(&cr).unwrap();
assert_eq!(json, r#"{"kind":"login"}"#);
let cr: CredentialResponse =
serde_json::from_str(r#"{"kind":"unknown-kind","extra-data":true}"#).unwrap();
assert_eq!(cr, CredentialResponse::Unknown);
let cr: CredentialResponse =
serde_json::from_str(r#"{"kind":"login","extra-data":true}"#).unwrap();
assert_eq!(cr, CredentialResponse::Login);
let cr: CredentialResponse = serde_json::from_str(r#"{"kind":"get","token":"value","cache":"never","operation_independent":true,"extra-field-ignored":123}"#).unwrap();
assert_eq!(
cr,
CredentialResponse::Get {
cache: CacheControl::Never,
operation_independent: true,
token: Secret::from("value".to_string())
}
);
}
#[test]
fn credential_request() {
let get_oweners = CredentialRequest {
v: PROTOCOL_VERSION_1,
args: vec![],
registry: RegistryInfo {
index_url: "url",
name: None,
headers: vec![],
},
action: Action::Get(Operation::Owners { name: "pkg" }),
};
let json = serde_json::to_string(&get_oweners).unwrap();
assert_eq!(
json,
r#"{"v":1,"registry":{"index-url":"url"},"kind":"get","operation":"owners","name":"pkg"}"#
);
let cr: CredentialRequest<'_> =
serde_json::from_str(r#"{"extra-1":true,"v":1,"registry":{"index-url":"url","extra-2":true},"kind":"get","operation":"owners","name":"pkg","args":[]}"#).unwrap();
assert_eq!(cr, get_oweners);
}
#[test]
fn credential_request_logout() {
let unknown = CredentialRequest {
v: PROTOCOL_VERSION_1,
args: vec![],
registry: RegistryInfo {
index_url: "url",
name: None,
headers: vec![],
},
action: Action::Logout,
};
let cr: CredentialRequest<'_> = serde_json::from_str(
r#"{"v":1,"registry":{"index-url":"url"},"kind":"logout","extra-1":true,"args":[]}"#,
)
.unwrap();
assert_eq!(cr, unknown);
}
#[test]
fn credential_request_unknown() {
let unknown = CredentialRequest {
v: PROTOCOL_VERSION_1,
args: vec![],
registry: RegistryInfo {
index_url: "",
name: None,
headers: vec![],
},
action: Action::Unknown,
};
let cr: CredentialRequest<'_> = serde_json::from_str(
r#"{"v":1,"registry":{"index-url":""},"kind":"unexpected-1","extra-1":true,"args":[]}"#,
)
.unwrap();
assert_eq!(cr, unknown);
}
}