use std::io::Read;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use lys_pass::Error;
use lys_pass::signin::{Secret, SignIn};
use super::config::{Lys, SignInBounds};
use super::signin::{Issuer, SignedIn, Started};
use crate::serve::pass::AUDIENCE;
const SECRET_BYTES: u64 = 4096;
pub struct LysSignIn {
sign_in: SignIn,
runtime: tokio::runtime::Runtime,
bounds: SignInBounds,
assistant: super::assistant_lys::Client,
}
impl LysSignIn {
pub fn connect(lys: &Lys) -> Result<Self, String> {
let bounds = SignInBounds::from_env()?;
let mut connected = Self::connect_with_timeouts(
lys,
std::time::Duration::from_millis(bounds.connect_ms.get()),
std::time::Duration::from_millis(bounds.total_ms.get()),
)?;
connected.bounds = bounds;
Ok(connected)
}
fn connect_with_timeouts(
lys: &Lys,
connect: std::time::Duration,
total: std::time::Duration,
) -> Result<Self, String> {
let secret = read_secret(&lys.secret_file)?;
let issuer = url::Url::parse(&lys.issuer)
.map_err(|_| format!("the lys issuer {} is not an address", lys.issuer))?;
let redirect = url::Url::parse(&lys.redirect)
.map_err(|_| format!("the lys redirect {} is not an address", lys.redirect))?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|error| format!("the door's sign-in runtime could not start: {error}"))?;
let http = reqwest::Client::builder()
.connect_timeout(connect)
.timeout(total)
.redirect(reqwest::redirect::Policy::none())
.no_proxy()
.build()
.map_err(|_| {
"lys_could_not_be_asked: the Lys HTTP client could not start".to_owned()
})?;
let sign_in = runtime
.block_on(SignIn::connect(
http.clone(),
&issuer,
AUDIENCE,
&lys.client_id,
secret,
redirect,
))
.map_err(|error| format!("{}: the lys issuer {}", error.name(), lys.issuer))?;
Ok(Self {
sign_in,
runtime,
bounds: SignInBounds::default(),
assistant: super::assistant_lys::Client {
issuer: lys.issuer.clone(),
http,
},
})
}
}
impl Issuer for LysSignIn {
fn assistant_identity(
&self,
agent: &str,
pass: &str,
store: &str,
) -> Result<String, crate::serve::storage::Failure> {
self.runtime
.block_on(self.assistant.identity(agent, pass, store, now()))
}
fn assistant_draft(
&self,
pass: &str,
draft: &str,
within: std::time::Duration,
) -> Result<serde_json::Value, crate::serve::storage::Failure> {
self.runtime
.block_on(self.assistant.draft(pass, draft, within))
}
fn assistant_decide(
&self,
pass: &str,
draft: &serde_json::Value,
decision: &str,
reason: Option<&str>,
within: std::time::Duration,
) -> Result<serde_json::Value, crate::serve::storage::Failure> {
self.runtime.block_on(
self.assistant
.decide_held(pass, draft, decision, reason, within),
)
}
fn assistant_url(&self) -> Result<&str, crate::serve::storage::Failure> {
Ok(&self.assistant.issuer)
}
fn bounds(&self) -> SignInBounds {
self.bounds
}
fn start(&self) -> Result<Started, Error> {
self.sign_in.start()
}
fn finish(
&self,
code: &str,
state: &str,
browser: &str,
started: &Started,
) -> Result<SignedIn, Error> {
self.runtime
.block_on(self.sign_in.finish(code, state, browser, started, now()))
}
fn refresh(&self, refresh: &str) -> Result<SignedIn, Error> {
self.runtime.block_on(self.sign_in.refresh(refresh, now()))
}
}
fn read_secret(path: &Path) -> Result<Secret, String> {
check_secret(path)?;
let unreadable = || format!("the lys secret_file {} could not be read", path.display());
let file = std::fs::File::open(path).map_err(|_| unreadable())?;
let mut text = String::new();
file.take(SECRET_BYTES + 1)
.read_to_string(&mut text)
.map_err(|_| unreadable())?;
if u64::try_from(text.len()).unwrap_or(u64::MAX) > SECRET_BYTES {
return Err(format!(
"the lys secret_file {} is longer than a credential",
path.display()
));
}
let line = text.strip_suffix('\n').unwrap_or(&text).to_owned();
Secret::new(line).map_err(|_| {
format!(
"the lys secret_file {} is not one line of credential",
path.display()
)
})
}
pub(super) fn check_secret(path: &Path) -> Result<(), String> {
use rustix::fs::{Access, AtFlags, CWD, accessat};
let unreadable = || format!("the lys secret_file {} could not be read", path.display());
std::fs::metadata(path).map_err(|_| unreadable())?;
accessat(CWD, path, Access::READ_OK, AtFlags::EACCESS).map_err(|_| unreadable())
}
fn now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs())
}
#[cfg(test)]
#[path = "lys_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "lys_deadline_tests.rs"]
mod deadline_tests;