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 futures::FutureExt;
13
use oneshot_fused_workaround as oneshot;
14
use serde::{Serialize, de::DeserializeOwned};
15
use std::path::{Path, PathBuf};
16
use std::sync::{Arc, Mutex};
17
use std::time::SystemTime;
18
use tor_error::warn_report;
19
use tracing::info;
20

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

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

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

            
85
40
        let statepath = mistrust
86
40
            .verifier()
87
40
            .check_content()
88
40
            .make_secure_dir(&dir)
89
40
            .map_err(|e| {
90
                Error::new(
91
                    e,
92
                    Action::Initializing,
93
                    Resource::Directory { dir: dir.clone() },
94
                )
95
            })?;
96
40
        let lockpath = statepath.join("state.lock").map_err(|e| {
97
            Error::new(
98
                e,
99
                Action::Initializing,
100
                Resource::Directory { dir: dir.clone() },
101
            )
102
        })?;
103

            
104
40
        let lockfile = Mutex::new(fslock::LockFile::open(&lockpath).map_err(|e| {
105
            Error::new(
106
                e,
107
                Action::Initializing,
108
                Resource::File {
109
                    container: dir,
110
                    file: "state.lock".into(),
111
                },
112
            )
113
        })?);
114

            
115
40
        let (lock_dropped_tx, lock_dropped_rx) = oneshot::channel();
116
40
        let lock_dropped_rx = lock_dropped_rx.shared();
117
40
        Ok(FsStateMgr {
118
40
            inner: Arc::new(FsStateMgrInner {
119
40
                statepath,
120
40
                lockfile,
121
40
                lock_dropped_tx,
122
40
                lock_dropped_rx,
123
40
            }),
124
40
        })
125
40
    }
126
    /// Like from_path_and_mistrust, but do not verify permissions.
127
    ///
128
    /// Testing only.
129
    #[cfg(test)]
130
14
    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
131
14
        Self::from_path_and_mistrust(
132
14
            path,
133
14
            &fs_mistrust::Mistrust::new_dangerously_trust_everyone(),
134
        )
135
14
    }
136

            
137
    /// Return a filename, relative to the top of this directory, to use for
138
    /// storing data with `key`.
139
    ///
140
    /// See "Limitations" section on [`FsStateMgr`] for caveats.
141
2321
    fn rel_filename(&self, key: &str) -> PathBuf {
142
2321
        (sanitize_filename::sanitize(key) + ".json").into()
143
2321
    }
144
    /// Return the top-level directory for this storage manager.
145
    ///
146
    /// (This is the same directory passed to
147
    /// [`FsStateMgr::from_path_and_mistrust`].)
148
298
    pub fn path(&self) -> &Path {
149
298
        self.inner
150
298
            .statepath
151
298
            .as_path()
152
298
            .parent()
153
298
            .expect("No parent directory even after path.join?")
154
298
    }
155

            
156
    /// Remove old and/or obsolete items from this storage manager.
157
    ///
158
    /// Requires that we hold the lock.
159
359
    fn clean(&self, now: SystemTime) {
160
359
        for fname in clean::files_to_delete(self.inner.statepath.as_path(), now) {
161
4
            info!("Deleting obsolete file {}", fname.anonymize_home());
162
4
            if let Err(e) = std::fs::remove_file(&fname) {
163
                warn_report!(e, "Unable to delete {}", fname.anonymize_home(),);
164
4
            }
165
        }
166
359
    }
167

            
168
    /// Operate using a `load_store::Target` for `key` in this state dir
169
106
    fn with_load_store_target<T, F>(&self, key: &str, action: Action, f: F) -> Result<T>
170
106
    where
171
106
        F: FnOnce(load_store::Target<'_>) -> std::result::Result<T, ErrorSource>,
172
    {
173
106
        let rel_fname = self.rel_filename(key);
174
106
        f(load_store::Target {
175
106
            dir: &self.inner.statepath,
176
106
            rel_fname: &rel_fname,
177
106
        })
178
106
        .map_err(|source| Error::new(source, action, self.err_resource(key)))
179
106
    }
180

            
181
    /// Return a `Resource` object representing the file with a given key.
182
100
    fn err_resource(&self, key: &str) -> Resource {
183
100
        Resource::File {
184
100
            container: self.path().to_path_buf(),
185
100
            file: PathBuf::from("state").join(self.rel_filename(key)),
186
100
        }
187
100
    }
188

            
189
    /// Return a `Resource` object representing our lock file.
190
    fn err_resource_lock(&self) -> Resource {
191
        Resource::File {
192
            container: self.path().to_path_buf(),
193
            file: "state.lock".into(),
194
        }
195
    }
196

            
197
    /// Return a handle which resolves when the file is unlocked
198
    pub fn wait_for_unlock(
199
        &self,
200
    ) -> impl futures::Future<Output = ()> + Send + Sync + 'static + use<> {
201
        self.inner.lock_dropped_rx.clone().map(|_| ())
202
    }
203
}
204

            
205
impl StateMgr for FsStateMgr {
206
1784
    fn can_store(&self) -> bool {
207
1784
        let lockfile = self
208
1784
            .inner
209
1784
            .lockfile
210
1784
            .lock()
211
1784
            .expect("Poisoned lock on state lockfile");
212
1784
        lockfile.owns_lock()
213
1784
    }
214
800
    fn try_lock(&self) -> Result<LockStatus> {
215
800
        let mut lockfile = self
216
800
            .inner
217
800
            .lockfile
218
800
            .lock()
219
800
            .expect("Poisoned lock on state lockfile");
220
800
        if lockfile.owns_lock() {
221
443
            Ok(LockStatus::AlreadyHeld)
222
357
        } else if lockfile
223
357
            .try_lock()
224
357
            .map_err(|e| Error::new(e, Action::Locking, self.err_resource_lock()))?
225
        {
226
355
            self.clean(SystemTime::now());
227
355
            Ok(LockStatus::NewlyAcquired)
228
        } else {
229
2
            Ok(LockStatus::NoLock)
230
        }
231
800
    }
232
2
    fn unlock(&self) -> Result<()> {
233
2
        let mut lockfile = self
234
2
            .inner
235
2
            .lockfile
236
2
            .lock()
237
2
            .expect("Poisoned lock on state lockfile");
238
2
        if lockfile.owns_lock() {
239
2
            lockfile
240
2
                .unlock()
241
2
                .map_err(|e| Error::new(e, Action::Unlocking, self.err_resource_lock()))?;
242
        }
243
2
        Ok(())
244
2
    }
245
66
    fn load<D>(&self, key: &str) -> Result<Option<D>>
246
66
    where
247
66
        D: DeserializeOwned,
248
    {
249
66
        self.with_load_store_target(key, Action::Loading, |t| t.load())
250
66
    }
251

            
252
46
    fn store<S>(&self, key: &str, val: &S) -> Result<()>
253
46
    where
254
46
        S: Serialize,
255
    {
256
46
        if !self.can_store() {
257
6
            return Err(Error::new(
258
6
                ErrorSource::NoLock,
259
6
                Action::Storing,
260
6
                Resource::Manager,
261
6
            ));
262
40
        }
263

            
264
40
        self.with_load_store_target(key, Action::Storing, |t| t.store(val))
265
46
    }
266
}
267

            
268
#[cfg(all(test, not(miri) /* filesystem access */))]
269
mod test {
270
    // @@ begin test lint list maintained by maint/add_warning @@
271
    #![allow(clippy::bool_assert_comparison)]
272
    #![allow(clippy::clone_on_copy)]
273
    #![allow(clippy::dbg_macro)]
274
    #![allow(clippy::mixed_attributes_style)]
275
    #![allow(clippy::print_stderr)]
276
    #![allow(clippy::print_stdout)]
277
    #![allow(clippy::single_char_pattern)]
278
    #![allow(clippy::unwrap_used)]
279
    #![allow(clippy::unchecked_duration_subtraction)]
280
    #![allow(clippy::useless_vec)]
281
    #![allow(clippy::needless_pass_by_value)]
282
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
283
    use super::*;
284
    use std::{collections::HashMap, time::Duration};
285

            
286
    #[test]
287
    fn simple() -> Result<()> {
288
        let dir = tempfile::TempDir::new().unwrap();
289
        let store = FsStateMgr::from_path(dir.path())?;
290

            
291
        assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
292
        let stuff: HashMap<_, _> = vec![("hello".to_string(), "world".to_string())]
293
            .into_iter()
294
            .collect();
295
        store.store("xyz", &stuff)?;
296

            
297
        let stuff2: Option<HashMap<String, String>> = store.load("xyz")?;
298
        let nothing: Option<HashMap<String, String>> = store.load("abc")?;
299

            
300
        assert_eq!(Some(stuff), stuff2);
301
        assert!(nothing.is_none());
302

            
303
        assert_eq!(dir.path(), store.path());
304

            
305
        drop(store); // Do this to release the fs lock.
306
        let store = FsStateMgr::from_path(dir.path())?;
307
        let stuff3: Option<HashMap<String, String>> = store.load("xyz")?;
308
        assert_eq!(stuff2, stuff3);
309

            
310
        let stuff4: HashMap<_, _> = vec![("greetings".to_string(), "humans".to_string())]
311
            .into_iter()
312
            .collect();
313

            
314
        assert!(matches!(
315
            store.store("xyz", &stuff4).unwrap_err().source(),
316
            ErrorSource::NoLock
317
        ));
318

            
319
        assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
320
        store.store("xyz", &stuff4)?;
321

            
322
        let stuff5: Option<HashMap<String, String>> = store.load("xyz")?;
323
        assert_eq!(Some(stuff4), stuff5);
324

            
325
        Ok(())
326
    }
327

            
328
    #[test]
329
    fn clean_successful() -> Result<()> {
330
        let dir = tempfile::TempDir::new().unwrap();
331
        let statedir = dir.path().join("state");
332
        let store = FsStateMgr::from_path(dir.path())?;
333

            
334
        assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
335
        let fname = statedir.join("numbat.toml");
336
        let fname2 = statedir.join("quoll.json");
337
        std::fs::write(fname, "we no longer use toml files.").unwrap();
338
        std::fs::write(fname2, "{}").unwrap();
339

            
340
        let count = statedir.read_dir().unwrap().count();
341
        assert_eq!(count, 3); // two files, one lock.
342

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

            
352
        Ok(())
353
    }
354

            
355
    #[cfg(target_family = "unix")]
356
    #[test]
357
    fn permissions() -> Result<()> {
358
        use std::fs::Permissions;
359
        use std::os::unix::fs::PermissionsExt;
360

            
361
        let ro_dir = Permissions::from_mode(0o500);
362
        let rw_dir = Permissions::from_mode(0o700);
363
        let unusable = Permissions::from_mode(0o000);
364

            
365
        let dir = tempfile::TempDir::new().unwrap();
366
        let statedir = dir.path().join("state");
367
        let store = FsStateMgr::from_path(dir.path())?;
368

            
369
        assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
370
        let fname = statedir.join("numbat.toml");
371
        let fname2 = statedir.join("quoll.json");
372
        std::fs::write(fname, "we no longer use toml files.").unwrap();
373
        std::fs::write(&fname2, "{}").unwrap();
374

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

            
388
        let h: Result<Option<HashMap<String, u32>>> = store.load("quoll");
389
        assert!(h.is_err());
390
        assert!(matches!(h.unwrap_err().source(), ErrorSource::IoError(_)));
391

            
392
        Ok(())
393
    }
394

            
395
    #[test]
396
    fn locking() {
397
        let dir = tempfile::TempDir::new().unwrap();
398
        let store1 = FsStateMgr::from_path(dir.path()).unwrap();
399
        let store2 = FsStateMgr::from_path(dir.path()).unwrap();
400

            
401
        // Nobody has the lock; store1 will take it.
402
        assert_eq!(store1.try_lock().unwrap(), LockStatus::NewlyAcquired);
403
        assert_eq!(store1.try_lock().unwrap(), LockStatus::AlreadyHeld);
404
        assert!(store1.can_store());
405

            
406
        // store1 has the lock; store2 will try to get it and fail.
407
        assert!(!store2.can_store());
408
        assert_eq!(store2.try_lock().unwrap(), LockStatus::NoLock);
409
        assert!(!store2.can_store());
410

            
411
        // Store 1 will drop the lock.
412
        store1.unlock().unwrap();
413
        assert!(!store1.can_store());
414
        assert!(!store2.can_store());
415

            
416
        // Now store2 can get the lock.
417
        assert_eq!(store2.try_lock().unwrap(), LockStatus::NewlyAcquired);
418
        assert!(store2.can_store());
419
        assert!(!store1.can_store());
420
    }
421

            
422
    #[test]
423
    fn errors() {
424
        let dir = tempfile::TempDir::new().unwrap();
425
        let store = FsStateMgr::from_path(dir.path()).unwrap();
426

            
427
        // file not found is not an error.
428
        let nonesuch: Result<Option<String>> = store.load("Hello");
429
        assert!(matches!(nonesuch, Ok(None)));
430

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