1use std::io::Read;
10use std::path::Path;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use lys_pass::Error;
14use lys_pass::signin::{Secret, SignIn};
15
16use super::config::{Lys, SignInBounds};
17use super::signin::{Issuer, SignedIn, Started};
18use crate::serve::pass::AUDIENCE;
19
20const SECRET_BYTES: u64 = 4096;
22
23pub struct LysSignIn {
25 sign_in: SignIn,
26 runtime: tokio::runtime::Runtime,
27 bounds: SignInBounds,
28 assistant: super::assistant_lys::Client,
29}
30
31impl LysSignIn {
32 pub fn connect(lys: &Lys) -> Result<Self, String> {
41 let bounds = SignInBounds::from_env()?;
42 let mut connected = Self::connect_with_timeouts(
43 lys,
44 std::time::Duration::from_millis(bounds.connect_ms.get()),
45 std::time::Duration::from_millis(bounds.total_ms.get()),
46 )?;
47 connected.bounds = bounds;
48 Ok(connected)
49 }
50
51 fn connect_with_timeouts(
52 lys: &Lys,
53 connect: std::time::Duration,
54 total: std::time::Duration,
55 ) -> Result<Self, String> {
56 let secret = read_secret(&lys.secret_file)?;
57 let issuer = url::Url::parse(&lys.issuer)
58 .map_err(|_| format!("the lys issuer {} is not an address", lys.issuer))?;
59 let redirect = url::Url::parse(&lys.redirect)
60 .map_err(|_| format!("the lys redirect {} is not an address", lys.redirect))?;
61 let runtime = tokio::runtime::Builder::new_current_thread()
62 .enable_all()
63 .build()
64 .map_err(|error| format!("the door's sign-in runtime could not start: {error}"))?;
65 let http = reqwest::Client::builder()
66 .connect_timeout(connect)
67 .timeout(total)
68 .redirect(reqwest::redirect::Policy::none())
69 .no_proxy()
70 .build()
71 .map_err(|_| {
72 "lys_could_not_be_asked: the Lys HTTP client could not start".to_owned()
73 })?;
74 let sign_in = runtime
75 .block_on(SignIn::connect(
76 http.clone(),
77 &issuer,
78 AUDIENCE,
79 &lys.client_id,
80 secret,
81 redirect,
82 ))
83 .map_err(|error| format!("{}: the lys issuer {}", error.name(), lys.issuer))?;
84 Ok(Self {
85 sign_in,
86 runtime,
87 bounds: SignInBounds::default(),
88 assistant: super::assistant_lys::Client {
89 issuer: lys.issuer.clone(),
90 http,
91 },
92 })
93 }
94}
95
96impl Issuer for LysSignIn {
97 fn assistant_identity(
98 &self,
99 agent: &str,
100 pass: &str,
101 store: &str,
102 ) -> Result<String, crate::serve::storage::Failure> {
103 self.runtime
104 .block_on(self.assistant.identity(agent, pass, store, now()))
105 }
106 fn assistant_draft(
107 &self,
108 pass: &str,
109 draft: &str,
110 within: std::time::Duration,
111 ) -> Result<serde_json::Value, crate::serve::storage::Failure> {
112 self.runtime
113 .block_on(self.assistant.draft(pass, draft, within))
114 }
115 fn assistant_decide(
116 &self,
117 pass: &str,
118 draft: &serde_json::Value,
119 decision: &str,
120 reason: Option<&str>,
121 within: std::time::Duration,
122 ) -> Result<serde_json::Value, crate::serve::storage::Failure> {
123 self.runtime.block_on(
124 self.assistant
125 .decide_held(pass, draft, decision, reason, within),
126 )
127 }
128 fn assistant_url(&self) -> Result<&str, crate::serve::storage::Failure> {
129 Ok(&self.assistant.issuer)
130 }
131 fn bounds(&self) -> SignInBounds {
132 self.bounds
133 }
134 fn start(&self) -> Result<Started, Error> {
135 self.sign_in.start()
136 }
137
138 fn finish(
139 &self,
140 code: &str,
141 state: &str,
142 browser: &str,
143 started: &Started,
144 ) -> Result<SignedIn, Error> {
145 self.runtime
146 .block_on(self.sign_in.finish(code, state, browser, started, now()))
147 }
148
149 fn refresh(&self, refresh: &str) -> Result<SignedIn, Error> {
150 self.runtime.block_on(self.sign_in.refresh(refresh, now()))
151 }
152}
153
154fn read_secret(path: &Path) -> Result<Secret, String> {
157 check_secret(path)?;
158 let unreadable = || format!("the lys secret_file {} could not be read", path.display());
159 let file = std::fs::File::open(path).map_err(|_| unreadable())?;
160 let mut text = String::new();
161 file.take(SECRET_BYTES + 1)
162 .read_to_string(&mut text)
163 .map_err(|_| unreadable())?;
164 if u64::try_from(text.len()).unwrap_or(u64::MAX) > SECRET_BYTES {
165 return Err(format!(
166 "the lys secret_file {} is longer than a credential",
167 path.display()
168 ));
169 }
170 let line = text.strip_suffix('\n').unwrap_or(&text).to_owned();
171 Secret::new(line).map_err(|_| {
172 format!(
173 "the lys secret_file {} is not one line of credential",
174 path.display()
175 )
176 })
177}
178
179pub(super) fn check_secret(path: &Path) -> Result<(), String> {
180 use rustix::fs::{Access, AtFlags, CWD, accessat};
181
182 let unreadable = || format!("the lys secret_file {} could not be read", path.display());
183 std::fs::metadata(path).map_err(|_| unreadable())?;
184 accessat(CWD, path, Access::READ_OK, AtFlags::EACCESS).map_err(|_| unreadable())
185}
186
187fn now() -> u64 {
190 SystemTime::now()
191 .duration_since(UNIX_EPOCH)
192 .map_or(0, |elapsed| elapsed.as_secs())
193}
194
195#[cfg(test)]
196#[path = "lys_tests.rs"]
197mod tests;
198
199#[cfg(test)]
200#[path = "lys_deadline_tests.rs"]
201mod deadline_tests;