1
//! Filesystem + JSON implementation of StateMgr.
2

            
3
#![forbid(unsafe_code)] // if you remove this, enable (or write) miri tests (git grep miri)
4

            
5
mod clean;
6

            
7
use crate::err::{Action, ErrorSource, Resource};
8
use crate::load_store;
9
use crate::{Error, LockStatus, Result, StateMgr};
10
use fs_mistrust::CheckedDir;
11
use fs_mistrust::anon_home::PathExt as _;
12
use fslock_guard::LockFileGuard;
13
use futures::FutureExt;
14
use oneshot_fused_workaround as oneshot;
15
use serde::{Serialize, de::DeserializeOwned};
16
use std::path::{Path, PathBuf};
17
use std::sync::{Arc, Mutex};
18
use tor_error::warn_report;
19
use tracing::info;
20
use web_time_compat::{SystemTime, SystemTimeExt};
21

            
22
/// Implementation of StateMgr that stores state as JSON files on disk.
23
///
24
/// # Locking
25
///
26
/// This manager uses a lock file to determine whether it's allowed to
27
/// write to the disk.  Only one process should write to the disk at
28
/// a time, though any number may read from the disk.
29
///
30
/// By default, every `FsStateMgr` starts out unlocked, and only able
31
/// to read.  Use [`FsStateMgr::try_lock()`] to lock it.
32
///
33
/// # Limitations
34
///
35
/// 1. This manager only accepts objects that can be serialized as
36
///    JSON documents.  Some types (like maps with non-string keys) can't
37
///    be serialized as JSON.
38
///
39
/// 2. This manager normalizes keys to an fs-safe format before saving
40
///    data with them.  This keeps you from accidentally creating or
41
///    reading files elsewhere in the filesystem, but it doesn't prevent
42
///    collisions when two keys collapse to the same fs-safe filename.
43
///    Therefore, you should probably only use ascii keys that are
44
///    fs-safe on all systems.
45
///
46
/// NEVER use user-controlled or remote-controlled data for your keys.
47
#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))]
48
#[derive(Clone, Debug)]
49
pub struct FsStateMgr {
50
    /// Inner reference-counted object.
51
    inner: Arc<FsStateMgrInner>,
52
}
53

            
54
/// Inner reference-counted object, used by `FsStateMgr`.
55
#[derive(Debug)]
56
struct FsStateMgrInner {
57
    /// Directory in which we store state files.
58
    statepath: CheckedDir,
59
    /// Lockfile to achieve exclusive access to state files.
60
    lockfile: Mutex<Option<LockFileGuard>>,
61
    /// A oneshot sender that is used to alert other tasks when this lock is
62
    /// finally dropped.
63
    ///
64
    /// It is a sender for Void because we never actually want to send anything here;
65
    /// we only want to generate canceled events.
66
    #[allow(dead_code)] // the only purpose of this field is to be dropped.
67
    lock_dropped_tx: oneshot::Sender<void::Void>,
68
    /// Cloneable handle which resolves when this lock is dropped.
69
    lock_dropped_rx: futures::future::Shared<oneshot::Receiver<void::Void>>,
70
}
71

            
72
impl FsStateMgr {
73
    /// Construct a new `FsStateMgr` to store data in `path`.
74
    ///
75
    /// This function will try to create `path` if it does not already
76
    /// exist.
77
    ///
78
    /// All files must be "private" according to the rules specified in `mistrust`.
79
48
    pub fn from_path_and_mistrust<P: AsRef<Path>>(
80
48
        path: P,
81
48
        mistrust: &fs_mistrust::Mistrust,
82
48
    ) -> Result<Self> {
83
48
        let path = path.as_ref();
84
48
        let dir = path.join("state");
85

            
86
48
        let statepath = mistrust
87
48
            .verifier()
88
48
            .check_content()
89
48
            .make_secure_dir(&dir)
90
48
            .map_err(|e| {
91
                Error::new(
92
                    e,
93
                    Action::Initializing,
94
                    Resource::Directory { dir: dir.clone() },
95
                )
96
            })?;
97

            
98
48
        let (lock_dropped_tx, lock_dropped_rx) = oneshot::channel();
99
48
        let lock_dropped_rx = lock_dropped_rx.shared();
100
48
        Ok(FsStateMgr {
101
48
            inner: Arc::new(FsStateMgrInner {
102
48
                statepath,
103
48
                lockfile: Mutex::new(None),
104
48
                lock_dropped_tx,
105
48
                lock_dropped_rx,
106
48
            }),
107
48
        })
108
48
    }
109
    /// Like from_path_and_mistrust, but do not verify permissions.
110
    ///
111
    /// Testing only.
112
    #[cfg(test)]
113
14
    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
114
14
        Self::from_path_and_mistrust(
115
14
            path,
116
14
            &fs_mistrust::Mistrust::new_dangerously_trust_everyone(),
117
        )
118
14
    }
119

            
120
    /// Return a filename, relative to the top of this directory, to use for
121
    /// storing data with `key`.
122
    ///
123
    /// See "Limitations" section on [`FsStateMgr`] for caveats.
124
394
    fn rel_filename(&self, key: &str) -> PathBuf {
125
394
        (sanitize_filename::sanitize(key) + ".json").into()
126
394
    }
127
    /// Return the top-level directory for this storage manager.
128
    ///
129
    /// (This is the same directory passed to
130
    /// [`FsStateMgr::from_path_and_mistrust`].)
131
286
    pub fn path(&self) -> &Path {
132
286
        self.inner
133
286
            .statepath
134
286
            .as_path()
135
286
            .parent()
136
286
            .expect("No parent directory even after path.join?")
137
286
    }
138

            
139
    /// Remove old and/or obsolete items from this storage manager.
140
    ///
141
    /// Requires that we hold the lock.
142
533
    fn clean(&self, now: SystemTime) {
143
533
        for fname in clean::files_to_delete(self.inner.statepath.as_path(), now) {
144
4
            info!("Deleting obsolete file {}", fname.anonymize_home());
145
4
            if let Err(e) = std::fs::remove_file(&fname) {
146
                warn_report!(e, "Unable to delete {}", fname.anonymize_home(),);
147
4
            }
148
        }
149
533
    }
150

            
151
    /// Operate using a `load_store::Target` for `key` in this state dir
152
28
    fn with_load_store_target<T, F>(&self, key: &str, action: Action, f: F) -> Result<T>
153
28
    where
154
28
        F: FnOnce(load_store::Target<'_>) -> std::result::Result<T, ErrorSource>,
155
    {
156
28
        let rel_fname = self.rel_filename(key);
157
28
        f(load_store::Target {
158
28
            dir: &self.inner.statepath,
159
28
            rel_fname: &rel_fname,
160
28
        })
161
28
        .map_err(|source| Error::new(source, action, self.err_resource(key)))
162
28
    }
163

            
164
    /// Return a `Resource` object representing the file with a given key.
165
96
    fn err_resource(&self, key: &str) -> Resource {
166
96
        Resource::File {
167
96
            container: self.path().to_path_buf(),
168
96
            file: PathBuf::from("state").join(self.rel_filename(key)),
169
96
        }
170
96
    }
171

            
172
    /// Return a `Resource` object representing our lock file.
173
    fn err_resource_lock(&self) -> Resource {
174
        Resource::File {
175
            container: self.path().to_path_buf(),
176
            file: "state.lock".into(),
177
        }
178
    }
179

            
180
    /// Return a handle which resolves when the file is unlocked
181
    pub fn wait_for_unlock(
182
        &self,
183
    ) -> impl futures::Future<Output = ()> + Send + Sync + 'static + use<> {
184
        self.inner.lock_dropped_rx.clone().map(|_| ())
185
    }
186
}
187

            
188
impl StateMgr for FsStateMgr {
189
114
    fn can_store(&self) -> bool {
190
114
        let lockfile = self
191
114
            .inner
192
114
            .lockfile
193
114
            .lock()
194
114
            .expect("Poisoned lock on state lockfile");
195
114
        lockfile.is_some()
196
114
    }
197

            
198
533
    fn try_lock(&self) -> Result<LockStatus> {
199
533
        let mut lockfile = self
200
533
            .inner
201
533
            .lockfile
202
533
            .lock()
203
533
            .expect("Poisoned lock on state lockfile");
204
533
        if lockfile.is_some() {
205
2
            return Ok(LockStatus::AlreadyHeld);
206
531
        }
207
531
        let lockpath = self.inner.statepath.join("state.lock").map_err(|e| {
208
            Error::new(
209
                e,
210
                Action::Initializing,
211
                Resource::Directory {
212
                    dir: self.inner.statepath.as_path().to_owned(),
213
                },
214
            )
215
        })?;
216

            
217
531
        let guard = LockFileGuard::try_lock(lockpath.as_path())
218
531
            .map_err(|e| Error::new(e, Action::Initializing, self.err_resource_lock()))?;
219
531
        *lockfile = guard;
220
531
        if lockfile.is_some() {
221
529
            self.clean(SystemTime::get());
222
529
            Ok(LockStatus::NewlyAcquired)
223
        } else {
224
2
            Ok(LockStatus::NoLock)
225
        }
226
533
    }
227

            
228
2
    fn unlock(&self) -> Result<()> {
229
2
        let mut lockfile = self
230
2
            .inner
231
2
            .lockfile
232
2
            .lock()
233
2
            .expect("Poisoned lock on state lockfile");
234

            
235
        // Dropping the guard will release the lock.
236
2
        let _guard: Option<LockFileGuard> = lockfile.take();
237
2
        Ok(())
238
2
    }
239
24
    fn load<D>(&self, key: &str) -> Result<Option<D>>
240
24
    where
241
24
        D: DeserializeOwned,
242
    {
243
24
        self.with_load_store_target(key, Action::Loading, |t| t.load())
244
24
    }
245

            
246
10
    fn store<S>(&self, key: &str, val: &S) -> Result<()>
247
10
    where
248
10
        S: Serialize,
249
    {
250
10
        if !self.can_store() {
251
6
            return Err(Error::new(
252
6
                ErrorSource::NoLock,
253
6
                Action::Storing,
254
6
                Resource::Manager,
255
6
            ));
256
4
        }
257

            
258
4
        self.with_load_store_target(key, Action::Storing, |t| t.store(val))
259
10
    }
260
}
261

            
262
#[cfg(all(test, not(miri) /* filesystem access */))]
263
mod test {
264
    // @@ begin test lint list maintained by maint/add_warning @@
265
    #![allow(clippy::bool_assert_comparison)]
266
    #![allow(clippy::clone_on_copy)]
267
    #![allow(clippy::dbg_macro)]
268
    #![allow(clippy::mixed_attributes_style)]
269
    #![allow(clippy::print_stderr)]
270
    #![allow(clippy::print_stdout)]
271
    #![allow(clippy::single_char_pattern)]
272
    #![allow(clippy::unwrap_used)]
273
    #![allow(clippy::unchecked_time_subtraction)]
274
    #![allow(clippy::useless_vec)]
275
    #![allow(clippy::needless_pass_by_value)]
276
    #![allow(clippy::string_slice)] // See arti#2571
277
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
278
    use super::*;
279
    use std::{collections::HashMap, time::Duration};
280

            
281
    #[test]
282
    fn simple() -> Result<()> {
283
        let dir = tempfile::TempDir::new().unwrap();
284
        let store = FsStateMgr::from_path(dir.path())?;
285

            
286
        assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
287
        let stuff: HashMap<_, _> = vec![("hello".to_string(), "world".to_string())]
288
            .into_iter()
289
            .collect();
290
        store.store("xyz", &stuff)?;
291

            
292
        let stuff2: Option<HashMap<String, String>> = store.load("xyz")?;
293
        let nothing: Option<HashMap<String, String>> = store.load("abc")?;
294

            
295
        assert_eq!(Some(stuff), stuff2);
296
        assert!(nothing.is_none());
297

            
298
        assert_eq!(dir.path(), store.path());
299

            
300
        drop(store); // Do this to release the fs lock.
301
        let store = FsStateMgr::from_path(dir.path())?;
302
        let stuff3: Option<HashMap<String, String>> = store.load("xyz")?;
303
        assert_eq!(stuff2, stuff3);
304

            
305
        let stuff4: HashMap<_, _> = vec![("greetings".to_string(), "humans".to_string())]
306
            .into_iter()
307
            .collect();
308

            
309
        assert!(matches!(
310
            store.store("xyz", &stuff4).unwrap_err().source(),
311
            ErrorSource::NoLock
312
        ));
313

            
314
        assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
315
        store.store("xyz", &stuff4)?;
316

            
317
        let stuff5: Option<HashMap<String, String>> = store.load("xyz")?;
318
        assert_eq!(Some(stuff4), stuff5);
319

            
320
        Ok(())
321
    }
322

            
323
    #[test]
324
    fn clean_successful() -> Result<()> {
325
        let dir = tempfile::TempDir::new().unwrap();
326
        let statedir = dir.path().join("state");
327
        let store = FsStateMgr::from_path(dir.path())?;
328

            
329
        assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
330
        let fname = statedir.join("numbat.toml");
331
        let fname2 = statedir.join("quoll.json");
332
        std::fs::write(fname, "we no longer use toml files.").unwrap();
333
        std::fs::write(fname2, "{}").unwrap();
334

            
335
        let count = statedir.read_dir().unwrap().count();
336
        assert_eq!(count, 3); // two files, one lock.
337

            
338
        // Now we can make sure that "clean" actually removes the right file.
339
        store.clean(SystemTime::get() + Duration::from_secs(365 * 86400));
340
        let lst: Vec<_> = statedir.read_dir().unwrap().collect();
341
        assert_eq!(lst.len(), 2); // one file, one lock.
342
        assert!(
343
            lst.iter()
344
                .any(|ent| ent.as_ref().unwrap().file_name() == "quoll.json")
345
        );
346

            
347
        Ok(())
348
    }
349

            
350
    #[cfg(target_family = "unix")]
351
    #[test]
352
    fn permissions() -> Result<()> {
353
        use std::fs::Permissions;
354
        use std::os::unix::fs::PermissionsExt;
355

            
356
        let ro_dir = Permissions::from_mode(0o500);
357
        let rw_dir = Permissions::from_mode(0o700);
358
        let unusable = Permissions::from_mode(0o000);
359

            
360
        let dir = tempfile::TempDir::new().unwrap();
361
        let statedir = dir.path().join("state");
362
        let store = FsStateMgr::from_path(dir.path())?;
363

            
364
        assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
365
        let fname = statedir.join("numbat.toml");
366
        let fname2 = statedir.join("quoll.json");
367
        std::fs::write(fname, "we no longer use toml files.").unwrap();
368
        std::fs::write(&fname2, "{}").unwrap();
369

            
370
        // Make the store directory read-only and make sure that we can't delete from it.
371
        std::fs::set_permissions(&statedir, ro_dir).unwrap();
372
        store.clean(SystemTime::get() + Duration::from_secs(365 * 86400));
373
        let lst: Vec<_> = statedir.read_dir().unwrap().collect();
374
        if lst.len() == 2 {
375
            // We must be root.  Don't do any more tests here.
376
            return Ok(());
377
        }
378
        assert_eq!(lst.len(), 3); // We can't remove the file, but we didn't freak out. Great!
379
        // Try failing to read a mode-0 file.
380
        std::fs::set_permissions(&statedir, rw_dir).unwrap();
381
        std::fs::set_permissions(fname2, unusable).unwrap();
382

            
383
        let h: Result<Option<HashMap<String, u32>>> = store.load("quoll");
384
        assert!(h.is_err());
385
        assert!(matches!(h.unwrap_err().source(), ErrorSource::IoError(_)));
386

            
387
        Ok(())
388
    }
389

            
390
    #[test]
391
    fn locking() {
392
        let dir = tempfile::TempDir::new().unwrap();
393
        let store1 = FsStateMgr::from_path(dir.path()).unwrap();
394
        let store2 = FsStateMgr::from_path(dir.path()).unwrap();
395

            
396
        // Nobody has the lock; store1 will take it.
397
        assert_eq!(store1.try_lock().unwrap(), LockStatus::NewlyAcquired);
398
        assert_eq!(store1.try_lock().unwrap(), LockStatus::AlreadyHeld);
399
        assert!(store1.can_store());
400

            
401
        // store1 has the lock; store2 will try to get it and fail.
402
        assert!(!store2.can_store());
403
        assert_eq!(store2.try_lock().unwrap(), LockStatus::NoLock);
404
        assert!(!store2.can_store());
405

            
406
        // Store 1 will drop the lock.
407
        store1.unlock().unwrap();
408
        assert!(!store1.can_store());
409
        assert!(!store2.can_store());
410

            
411
        // Now store2 can get the lock.
412
        assert_eq!(store2.try_lock().unwrap(), LockStatus::NewlyAcquired);
413
        assert!(store2.can_store());
414
        assert!(!store1.can_store());
415
    }
416

            
417
    #[test]
418
    fn errors() {
419
        let dir = tempfile::TempDir::new().unwrap();
420
        let store = FsStateMgr::from_path(dir.path()).unwrap();
421

            
422
        // file not found is not an error.
423
        let nonesuch: Result<Option<String>> = store.load("Hello");
424
        assert!(matches!(nonesuch, Ok(None)));
425

            
426
        // bad utf8 is an error.
427
        let file: PathBuf = ["state", "Hello.json"].iter().collect();
428
        std::fs::write(dir.path().join(&file), b"hello world \x00\xff").unwrap();
429
        let bad_utf8: Result<Option<String>> = store.load("Hello");
430
        assert!(bad_utf8.is_err());
431
        assert_eq!(
432
            bad_utf8.unwrap_err().to_string(),
433
            format!(
434
                "IO error while loading persistent data on {} in {}",
435
                file.to_string_lossy(),
436
                dir.path().anonymize_home(),
437
            ),
438
        );
439
    }
440
}