rig_core/providers/internal/auth/
store.rs1use crate::error::BoxError;
5use crate::wasm_compat::{WasmBoxedFuture, WasmCompatSend, WasmCompatSync};
6use std::collections::HashMap;
7use std::sync::{Arc, Mutex, PoisonError};
8
9pub trait TokenStore: WasmCompatSend + WasmCompatSync {
31 fn load<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<Option<String>, BoxError>>;
34
35 fn save<'a>(
37 &'a self,
38 key: &'a str,
39 record: &'a str,
40 ) -> WasmBoxedFuture<'a, Result<(), BoxError>>;
41
42 fn delete<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<(), BoxError>>;
44
45 fn lock<'a>(&'a self, _key: &'a str) -> WasmBoxedFuture<'a, Result<StoreLock, BoxError>> {
51 Box::pin(async { Ok(StoreLock::new(())) })
52 }
53}
54
55pub struct StoreLock {
58 _guard: Mutex<Box<dyn WasmCompatSend>>,
61}
62
63impl StoreLock {
64 pub fn new(guard: impl WasmCompatSend + 'static) -> Self {
73 Self {
74 _guard: Mutex::new(Box::new(guard)),
75 }
76 }
77}
78
79impl std::fmt::Debug for StoreLock {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.debug_struct("StoreLock").finish_non_exhaustive()
82 }
83}
84
85impl<T: TokenStore + ?Sized> TokenStore for Arc<T> {
86 fn load<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<Option<String>, BoxError>> {
87 (**self).load(key)
88 }
89
90 fn save<'a>(
91 &'a self,
92 key: &'a str,
93 record: &'a str,
94 ) -> WasmBoxedFuture<'a, Result<(), BoxError>> {
95 (**self).save(key, record)
96 }
97
98 fn delete<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<(), BoxError>> {
99 (**self).delete(key)
100 }
101
102 fn lock<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<StoreLock, BoxError>> {
103 (**self).lock(key)
104 }
105}
106
107#[derive(Default)]
109pub struct MemoryTokenStore(Mutex<HashMap<String, String>>);
110
111impl std::fmt::Debug for MemoryTokenStore {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.debug_struct("MemoryTokenStore").finish_non_exhaustive()
114 }
115}
116
117impl MemoryTokenStore {
118 fn records(&self) -> std::sync::MutexGuard<'_, HashMap<String, String>> {
119 self.0.lock().unwrap_or_else(PoisonError::into_inner)
120 }
121}
122
123impl TokenStore for MemoryTokenStore {
124 fn load<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<Option<String>, BoxError>> {
125 Box::pin(async move { Ok(self.records().get(key).cloned()) })
126 }
127
128 fn save<'a>(
129 &'a self,
130 key: &'a str,
131 record: &'a str,
132 ) -> WasmBoxedFuture<'a, Result<(), BoxError>> {
133 Box::pin(async move {
134 self.records().insert(key.to_owned(), record.to_owned());
135 Ok(())
136 })
137 }
138
139 fn delete<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<(), BoxError>> {
140 Box::pin(async move {
141 self.records().remove(key);
142 Ok(())
143 })
144 }
145}
146
147#[cfg(not(target_family = "wasm"))]
153#[derive(Debug, Clone)]
154pub struct FileTokenStore {
155 dir: std::path::PathBuf,
156}
157
158#[cfg(not(target_family = "wasm"))]
159impl FileTokenStore {
160 pub fn new(dir: impl Into<std::path::PathBuf>) -> Self {
162 Self { dir: dir.into() }
163 }
164
165 pub fn path(&self, key: &str) -> std::path::PathBuf {
167 self.dir.join(format!("{key}.json"))
168 }
169}
170
171#[cfg(not(target_family = "wasm"))]
172impl TokenStore for FileTokenStore {
173 fn load<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<Option<String>, BoxError>> {
174 Box::pin(async move {
175 match std::fs::read_to_string(self.path(key)) {
176 Ok(record) => Ok(Some(record)),
177 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
178 Err(error) => Err(error.into()),
179 }
180 })
181 }
182
183 fn save<'a>(
184 &'a self,
185 key: &'a str,
186 record: &'a str,
187 ) -> WasmBoxedFuture<'a, Result<(), BoxError>> {
188 Box::pin(async move { Ok(write_private(&self.path(key), record.as_bytes())?) })
189 }
190
191 fn delete<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<(), BoxError>> {
192 Box::pin(async move {
193 match std::fs::remove_file(self.path(key)) {
194 Err(error) if error.kind() != std::io::ErrorKind::NotFound => Err(error.into()),
195 _ => Ok(()),
196 }
197 })
198 }
199
200 fn lock<'a>(&'a self, key: &'a str) -> WasmBoxedFuture<'a, Result<StoreLock, BoxError>> {
204 Box::pin(async move {
205 let path = self.dir.join(format!("{key}.lock"));
206 create_private_dir(&path)?;
207 let mut options = std::fs::OpenOptions::new();
208 options.read(true).write(true).create(true).truncate(false);
209 #[cfg(unix)]
210 std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
211 let file = options.open(&path)?;
212 let started = std::time::Instant::now();
213 loop {
214 match file.try_lock() {
215 Ok(()) => return Ok(StoreLock::new(file)),
216 Err(std::fs::TryLockError::WouldBlock) if started.elapsed() < LOCK_WAIT => {
217 crate::wasm_compat::sleep(std::time::Duration::from_millis(50)).await;
218 }
219 Err(std::fs::TryLockError::WouldBlock) => {
220 return Err(format!(
221 "another process has held {} for {} seconds",
222 path.display(),
223 LOCK_WAIT.as_secs()
224 )
225 .into());
226 }
227 Err(std::fs::TryLockError::Error(error)) => return Err(error.into()),
228 }
229 }
230 })
231 }
232}
233
234#[cfg(not(target_family = "wasm"))]
237const LOCK_WAIT: std::time::Duration = std::time::Duration::from_secs(120);
238
239#[cfg(not(target_family = "wasm"))]
241fn create_private_dir(path: &std::path::Path) -> Result<(), std::io::Error> {
242 let Some(parent) = path.parent() else {
243 return Ok(());
244 };
245 let mut builder = std::fs::DirBuilder::new();
246 builder.recursive(true);
247 #[cfg(unix)]
248 std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
249 builder.create(parent)
250}
251
252#[cfg(not(target_family = "wasm"))]
259fn write_private(path: &std::path::Path, bytes: &[u8]) -> Result<(), std::io::Error> {
260 use std::io::Write;
261 use std::sync::atomic::{AtomicU64, Ordering};
262
263 static WRITES: AtomicU64 = AtomicU64::new(0);
264 create_private_dir(path)?;
265 let mut temp = path.as_os_str().to_owned();
266 temp.push(format!(
267 ".tmp-{}-{}",
268 std::process::id(),
269 WRITES.fetch_add(1, Ordering::Relaxed)
270 ));
271 let temp = std::path::PathBuf::from(temp);
272 let write = || {
273 let mut options = std::fs::OpenOptions::new();
274 options.write(true).create_new(true);
275 #[cfg(unix)]
276 std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
277 let mut file = options.open(&temp)?;
278 #[cfg(unix)]
279 file.set_permissions(std::os::unix::fs::PermissionsExt::from_mode(0o600))?;
280 file.write_all(bytes)?;
281 file.sync_all()?;
282 std::fs::rename(&temp, path)
283 };
284 let written = write();
285 if written.is_err() {
286 std::fs::remove_file(&temp).ok();
287 }
288 written
289}
290
291pub(crate) async fn load_record<T: Default + serde::de::DeserializeOwned>(
293 store: &dyn TokenStore,
294 key: &str,
295) -> Result<T, super::AuthError> {
296 match store.load(key).await.map_err(super::AuthError::Store)? {
297 Some(record) => Ok(serde_json::from_str(&record)?),
298 None => Ok(T::default()),
299 }
300}
301
302pub(crate) async fn save_record<T: serde::Serialize>(
304 store: &dyn TokenStore,
305 key: &str,
306 record: &T,
307) -> Result<(), super::AuthError> {
308 let text = serde_json::to_string_pretty(record)?;
309 store
310 .save(key, &text)
311 .await
312 .map_err(super::AuthError::Store)
313}
314
315#[cfg(test)]
316mod tests;