aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--crates/tor-hsservice/Cargo.toml1
-rw-r--r--crates/tor-hsservice/src/lib.rs7
-rw-r--r--crates/tor-hsservice/src/nickname.rs2
-rw-r--r--crates/tor-hsservice/src/state.rs4
-rw-r--r--crates/tor-hsservice/src/state_dir.md74
-rw-r--r--crates/tor-hsservice/src/state_dir.rs579
-rw-r--r--crates/tor-netdoc/src/doc/hsdesc/build.rs2
-rw-r--r--crates/tor-netdoc/src/doc/hsdesc/build/inner.rs5
9 files changed, 669 insertions, 6 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 86e04b0e0..58ea95f88 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5373,6 +5373,7 @@ dependencies = [
name = "tor-hsservice"
version = "0.6.0"
dependencies = [
+ "amplify",
"anyhow",
"async-trait",
"base64ct",
diff --git a/crates/tor-hsservice/Cargo.toml b/crates/tor-hsservice/Cargo.toml
index dff3c793e..a1e9d15e1 100644
--- a/crates/tor-hsservice/Cargo.toml
+++ b/crates/tor-hsservice/Cargo.toml
@@ -38,6 +38,7 @@ full = [
]
[dependencies]
+amplify = { version = "4", default-features = false, features = ["derive"] }
async-trait = "0.1.54"
base64ct = "1.5.1"
derive-adhoc = "0.8"
diff --git a/crates/tor-hsservice/src/lib.rs b/crates/tor-hsservice/src/lib.rs
index 11d84c093..c04a23ce5 100644
--- a/crates/tor-hsservice/src/lib.rs
+++ b/crates/tor-hsservice/src/lib.rs
@@ -73,6 +73,9 @@ pub mod status;
mod svc;
mod timeout_track;
+#[rustfmt::skip] // to let us have inline { todo!() }; TODO HSS remove
+mod state_dir;
+
// rustdoc doctests can't use crate-public APIs, so are broken if provided for private items.
// So we export the whole module again under this name.
// Supports the Example in timeout_track.rs's module-level docs.
@@ -87,6 +90,10 @@ pub mod timeout_track_for_doctests_unstable_no_semver_guarantees {
pub use crate::timeout_track::*;
}
#[doc(hidden)]
+pub mod state_dir_for_doctests_unstable_no_semver_guarantees {
+ pub use crate::state_dir::*;
+}
+#[doc(hidden)]
pub mod time_store_for_doctests_unstable_no_semver_guarantees {
pub use crate::time_store::*;
}
diff --git a/crates/tor-hsservice/src/nickname.rs b/crates/tor-hsservice/src/nickname.rs
index 73bc88ca7..9948399a6 100644
--- a/crates/tor-hsservice/src/nickname.rs
+++ b/crates/tor-hsservice/src/nickname.rs
@@ -110,7 +110,7 @@ mod test {
#[test]
fn serde() {
- // TODO HSS clone-and-hack with tor_keymgr::::key_specifier::test::serde
+ // TODO: clone-and-hack with tor_keymgr::::key_specifier::test::serde
#[derive(Serialize, Deserialize, Debug)]
struct T {
n: HsNickname,
diff --git a/crates/tor-hsservice/src/state.rs b/crates/tor-hsservice/src/state.rs
index 876f4bb65..442edb54d 100644
--- a/crates/tor-hsservice/src/state.rs
+++ b/crates/tor-hsservice/src/state.rs
@@ -11,7 +11,7 @@ use crate::{HsIdPublicKeySpecifier, HsNickname};
/// A helper for managing the persistent state of hidden services.
//
-// TODO HSS decide what API we want here and implement it
+// TODO (#1220) decide what API we want here and implement it
// See https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/1837#note_2977513
pub struct StateMgr {
/// The key manager
@@ -23,7 +23,7 @@ impl StateMgr {
pub fn new(keystore_dir: impl AsRef<Path>, permissions: &Mistrust) -> tor_keymgr::Result<Self> {
let arti_store = ArtiNativeKeystore::from_path_and_mistrust(&keystore_dir, permissions)?;
- // TODO HSS: make the default store configurable
+ // TODO (#1106): make the default store configurable
let default_store = arti_store;
let keymgr = KeyMgrBuilder::default()
diff --git a/crates/tor-hsservice/src/state_dir.md b/crates/tor-hsservice/src/state_dir.md
new file mode 100644
index 000000000..03dde2e9c
--- /dev/null
+++ b/crates/tor-hsservice/src/state_dir.md
@@ -0,0 +1,74 @@
+# LISTING OF AN ARTI STATE DIRECTORY for planning `state_dir.rs`
+
+TODO HSS delete these random notes at some point
+(possibly after making extra docs somewhere about these things?)
+
+```text
+keymgr
+
+ keystore/
+
+dirmgr
+not in state/ because ???
+storage.rs ad hoc sqlite3
+EXPIRY built-in
+
+ dir_blobs/
+ dir_blobs/con:microdesc:sha3-256-3f4d5d6519d51b20d7161d3f12cb7e23114d0f0f4d252a73077dfe9719011962
+ dir.sqlite3
+
+tor_persist
+FsStateMgr
+.local/share/arti/state.lock
+
+ state/
+ state/state.lock
+
+guardmgr
+via storage handle
+EXPIRTY singleton
+
+ state/guards.json
+
+circmgr?
+via storage handle
+EXPIRTY singleton
+
+ state/circuit_timeouts.json
+
+ipt_mgr x2
+via storage handle
+*no* lock against multiple instantiation of same HS
+EXPIRTY needs to linked to hs (ie to instance)
+
+ state/hs_iptpub_ztest.json
+ state/hs_ipts_ztest.json
+
+?
+not per instance ?
+should it be ?
+no locking ?
+ pt_state/
+
+?
+ 3161169 0 -rw-r--r-- 1 rustcargo rustcargo 0 Apr 28 2022 .local/share/arti/state.lock
+ state.lock
+
+HS IPT replay log
+ad-hoc via Path
+lock against multiple instantiation of same HS
+secondary internal lock via mutex
+EXPIRY whole dir needs to be linked to hs
+EXPIRY needs internal expiry mechanism too
+
+ hss_iptreplay/
+ hss_iptreplay/replay_ztest/
+ hss_iptreplay/replay_ztest/lock
+ hss_iptreplay/replay_ztest/9aa9517e6901c280a550911d3a3c679630403db1c622eedefbdf1715297f795f.bin
+ hss_iptreplay/replay_ztest/92d897263497b7e9f998bc7b14cb60a09bfb1beb418cc8266b7e1cc36709b3bf.bin
+ hss_iptreplay/replay_ztest/816885a3bf50c90f659304406bde9df0ec926c4b62c556a423b0f6ee7e646c0c.bin
+ hss_iptreplay/replay_ztest/62355f8672a24cd76e3f6f769fdb66f829aca898824f88cd16f5dca12cab0fd1.bin
+ hss_iptreplay/replay_ztest/7c3bc3ff6f8737b29bc54fd2dd0addf598dad2b69a3993e9f962c512fa42d6f7.bin
+ hss_iptreplay/replay_ztest/a35dab4c73c2a2492833c38ea127eb64dfd62b03104b570cd303069e45db9cbb.bin
+
+```
diff --git a/crates/tor-hsservice/src/state_dir.rs b/crates/tor-hsservice/src/state_dir.rs
new file mode 100644
index 000000000..b14474bfb
--- /dev/null
+++ b/crates/tor-hsservice/src/state_dir.rs
@@ -0,0 +1,579 @@
+//! State helper utility
+//!
+//! All the methods in this module perform appropriate mistrust checks.
+//!
+//! All the methods arrange to ensure suitably-finegrained exclusive access.
+//! "Read-only" or "shared" mode is not supported.
+//!
+//! ### Differences from `tor_persist::StorageHandle`
+//!
+//! * Explicit provision is made for multiple instances of a single facility.
+//! For example, multiple hidden services,
+//! each with their own state, and own lock.
+//!
+//! * Locking (via filesystem locks) is mandatory, rather than optional -
+//! there is no "shared" mode.
+//!
+//! * Locked state is represented in the Rust type system.
+//!
+//! * We don't use traits to support multiple implementations.
+//! Platform support would be done in the future with `#[cfg]`.
+//! Testing is done by temporary directories (as currently with `tor_persist`).
+//!
+//! * The serde-based `StorageHandle` requires `&mut` for writing.
+//! This ensures proper serialisation of 1. read-modify-write cycles
+//! and 2. use of the temporary file.
+//! Or to put it another way, we model `StorageHandle`
+//! as *containing* a `T` without interior mutability.
+//!
+//! * There's a way to get a raw directory for filesystem operations
+//! (currently, will be used for IPT replay logs).
+//!
+//! ### Implied filesystem structure
+//!
+//! ```text
+//! STATE_DIR/
+//! STATE_DIR/KIND/INSTANCE/
+//! STATE_DIR/KIND/INSTANCE/lock
+//! STATE_DIR/KIND/INSTANCE/SLUG.json
+//! STATE_DIR/KIND/INSTANCE/SLUG.new
+//! STATE_DIR/KIND/INSTANCE/SLUG/
+//!
+//! eg
+//!
+//! STATE_DIR/hss/allium-cepa.lock
+//! STATE_DIR/hss/allium-cepa/ipts.json
+//! STATE_DIR/hss/allium-cepa/iptpub.json
+//! STATE_DIR/hss/allium-cepa/iptreplay/
+//! STATE_DIR/hss/allium-cepa/iptreplay/9aa9517e6901c280a550911d3a3c679630403db1c622eedefbdf1715297f795f.bin
+//! ```
+//!
+//! (The lockfile is outside the instance directory to facilitate
+//! concurrency-correct deletion.)
+//!
+//! ### Comprehensive example
+//!
+//! ```
+//! use std::{collections::HashSet, fmt, time::Duration};
+//! use tor_error::{into_internal, Bug};
+//! # use tor_hsservice::state_dir_for_doctests_unstable_no_semver_guarantees as state_dir;
+//! # #[cfg(all)] // works like #[cfg(FALSE)]. Instead, we have this workaround ^.
+//! use crate::state_dir;
+//! use state_dir::{InstanceIdString, InstanceIdentity, InstancePurgeHandler};
+//! use state_dir::{InstancePurgeInfo, InstanceStateHandle, StateDirectory, StorageHandle};
+//! #
+//! # // fake up some things; we do this rather than using real ones
+//! # // since this example will move, with the module, to a lower level crate.
+//! # struct OnionService { }
+//! # #[derive(derive_more::Display)] struct HsNickname(String);
+//! # type Error = anyhow::Error;
+//! # mod ipt_mgr { pub mod persist {
+//! # #[derive(serde::Serialize, serde::Deserialize)] pub struct StateRecord {}
+//! # } }
+//!
+//! impl InstanceIdentity for HsNickname {
+//! fn kind() -> &'static str { "hss" }
+//! fn write_identity(&self, f: &mut fmt::Formatter) -> Result<(), Bug> {
+//! write!(f, "{self}").map_err(into_internal!("failed to write HS nickname"))
+//! }
+//! }
+//!
+//! impl OnionService {
+//! fn new(
+//! nick: HsNickname,
+//! state_dir: &StateDirectory,
+//! ) -> Result<Self, Error> {
+//! let instance_state = state_dir.acquire_instance(&nick)?;
+//! let replay_log_dir = instance_state.raw_subdir("ipt_replay")?;
+//! let ipts_storage: StorageHandle<ipt_mgr::persist::StateRecord> =
+//! instance_state.storage_handle("ipts")?;
+//! // ..
+//! # Ok(OnionService { })
+//! }
+//! }
+//!
+//! struct PurgeHandler<'h>(&'h HashSet<&'h str>, Duration);
+//! impl InstancePurgeHandler for PurgeHandler<'_> {
+//! fn name_filter(&mut self, id: &InstanceIdString)
+//! -> state_dir::Result<state_dir::Liveness> {
+//! Ok(if self.0.contains(id.as_str()) {
+//! state_dir::Liveness::Live
+//! } else {
+//! state_dir::Liveness::PossiblyUnused
+//! })
+//! }
+//! fn retain_unused_for(&mut self, id: &InstanceIdString) -> state_dir::Result<Duration> {
+//! Ok(self.1)
+//! }
+//! fn dispose(&mut self, _info: &InstancePurgeInfo, handle: InstanceStateHandle)
+//! -> state_dir::Result<()> {
+//! // here might be a good place to delete keys too
+//! handle.delete()
+//! }
+//! }
+//! pub fn expire_hidden_services(
+//! state_dir: &StateDirectory,
+//! currently_configured_nicks: &HashSet<&str>,
+//! retain_for: Duration,
+//! ) -> Result<(), Error> {
+//! state_dir.purge_instances(&mut PurgeHandler(currently_configured_nicks, retain_for))?;
+//! Ok(())
+//! }
+//! ```
+//!
+//! ### Platforms without a filesystem
+//!
+//! The implementation and (in places) the documentation
+//! is in terms of filesystems.
+//! But, everything except `InstanceStateHandle::raw_subdir`
+//! is abstract enough to implement some other way.
+//!
+//! If we wish to support such platforms, the approach is:
+//!
+//! * Decide on an approach for `StorageHandle`
+//! and for each caller of `raw_subdir`.
+//!
+//! * Figure out how the startup code will look.
+//! (Currently everything is in terms of `fs_mistrust` and filesystems.)
+//!
+//! * Provide a version of this module with a compatible API
+//! in terms of whatever underlying facilities are available.
+//! Use `#[cfg]` to select it.
+//! Don't implement `raw_subdir`.
+//!
+//! * Call sites using `raw_subdir` will no longer compile.
+//! Use `#[cfg]` at call sites to replace the `raw_subdir`
+//! with whatever is appropriate for the platform.
+
+#![allow(unused_variables, dead_code)]
+#![allow(unreachable_pub)] // TODO this module will hopefully move to tor-persist and be pub
+
+use std::cell::Cell;
+use std::fmt;
+use std::iter;
+use std::marker::PhantomData;
+use std::path::Path;
+use std::sync::Arc;
+use std::time::{Duration, SystemTime};
+
+use derive_more::{AsRef, Deref, Into};
+use serde::{de::DeserializeOwned, Serialize};
+use thiserror::Error;
+use void::Void;
+
+use fs_mistrust::{CheckedDir, Mistrust};
+use tor_error::Bug;
+
+/// TODO HSS remove
+type Todo = Void;
+
+use std::result::Result as StdResult;
+
+/// [`Result`](StdResult) throwing a [`state_dir::Error`](enum@Error)
+pub type Result<T> = StdResult<T, Error>;
+
+/// The whole program's state directory
+///
+/// Representation of `[storage] state_dir` and `permissions`
+/// from the Arti configuration.
+///
+/// This type does not embody any subpaths relating to
+/// any particular facility within Arti.
+///
+/// Constructing a `StateDirectory` may involve filesystem permissions checks,
+/// so ideally it would be created once per process for performance reasons.
+///
+/// Existence of a `StateDirectory` also does not imply exclusive access.
+///
+/// This type is passed to each facility's constructor;
+/// the facility implements [`InstanceIdentity`]
+/// and calls [`acquire_instance`](StateDirectory::acquire_instance).
+///
+/// ### Use for caches
+///
+/// In principle this type and the methods and subtypes available
+/// would be suitable for cache data as well as state data.
+///
+/// However the locking scheme does not tolerate random removal of files.
+/// And cache directories are sometimes configured to point to locations
+/// with OS-supplied automatic file cleaning.
+/// That would not be correct,
+/// since the automatic file cleaner might remove an in-use lockfile,
+/// effectively unlocking the instance state
+/// even while a process exists that thinks it still has the lock.
+#[allow(clippy::missing_docs_in_private_items)] // TODO HSS remove
+pub struct StateDirectory {
+ path: Todo,
+ mistrust: Todo,
+}
+
+/// An instance of a facility that wants to save persistent state (caller-provided impl)
+///
+/// Each value of a type implementing `InstanceIdentity`
+/// designates a specific instance of a specific facility.
+///
+/// For example, `HsNickname` implements `state_dir::InstanceIdentity`.
+///
+/// The kind and identity are strings from a restricted character set:
+/// Only lowercase ASCII alphanumerics, `_` , and `+`, are permitted,
+/// and the first character must be an ASCII alphanumeric.
+///
+/// (The output from `write_identity` will be converted to an [`InstanceIdString`].)
+pub trait InstanceIdentity {
+ /// Return the kind. For example `hss` for a Tor Hidden Service.
+ ///
+ /// This must return a fixed string,
+ /// since usually all instances represented the same Rust type
+ /// are also the same kind.
+ //
+ // This precludes dynamically chosen instance kind identifiers.
+ // If we ever want that, we'd need an InstanceKind trait that is implemented
+ // not for actual instances, but for values representing a kind.
+ fn kind() -> &'static str;
+
+ /// Obtain identity
+ ///
+ /// The instance identity distinguishes different instances of the same kind.
+ ///
+ /// For example, for a Tor Hidden Service the identity is the nickname.
+ //
+ // Throws Bug rather than fmt::Error so that in case of problems we can dump a stack trace.
+ fn write_identity(&self, f: &mut fmt::Formatter) -> StdResult<(), Bug>;
+}
+
+/// For a facility to be expired using [`purge_instances`](StateDirectory::purge_instances) (caller-provided impl)
+///
+/// A filter which decides which instances to delete,
+/// and deletes them if appropriate.
+///
+/// See [`purge_instances`](StateDirectory::purge_instances) for full documentation.
+pub trait InstancePurgeHandler {
+ /// Can we tell by its name that this instance is still live ?
+ fn name_filter(&mut self, identity: &InstanceIdString) -> Result<Liveness>;
+
+ /// How long should we retain an unused instance for ?
+ ///
+ /// Many implementations won't need to use `identity`.
+ /// To pass every possibly-unused instance
+ /// through to `dispose`, return `Duration::ZERO`.
+ fn retain_unused_for(&mut self, identity: &InstanceIdString) -> Result<Duration>;
+
+ /// Decide whether to keep this instance
+ ///
+ /// When it has made its decision, `dispose` should
+ /// either call [`delete`](InstanceStateHandle::delete),
+ /// or simply drop `handle`.
+ ///
+ /// Called only after `name_filter` returned [`Liveness::PossiblyUnused`]
+ /// and only if the instance has not been acquired or modified recently.
+ ///
+ /// `info` includes the instance name and other useful information
+ /// such as the last modification time.
+ fn dispose(&mut self, info: &InstancePurgeInfo, handle: InstanceStateHandle) -> Result<()>;
+}
+
+/// Information about an instance, passed to [`InstancePurgeHandler::dispose`]
+#[derive(amplify::Getters)]
+#[derive(AsRef)]
+pub struct InstancePurgeInfo<'i> {
+ /// The instance's identity string
+ #[as_ref]
+ identity: &'i InstanceIdString,
+
+ /// When the instance state was last updated, according to the filesystem timestamps
+ ///
+ /// See `[InstanceStateHandle::purge_instances]`
+ /// for details of what kinds of events count as modifications.
+ last_modified: SystemTime,
+}
+
+/// String identifying an instance, within its kind
+///
+/// Instance identities are from a restricted character set.
+/// See [`InstanceIdentity`].
+#[derive(Into, derive_more::Display)]
+pub struct InstanceIdString(String);
+
+impl InstanceIdString {
+ /// Obtain this `InstanceIdString` as a `&str`
+ pub fn as_str(&self) -> &str {
+ self.as_ref()
+ }
+}
+impl AsRef<str> for InstanceIdString {
+ fn as_ref(&self) -> &str {
+ &self.0
+ }
+}
+
+impl TryFrom<String> for InstanceIdString {
+ // TODO this should probably be a general InvalidSlug from a lower-level Slug type
+ type Error = Bug;
+ fn try_from(s: String) -> StdResult<Self, Self::Error> {
+ todo!()
+ }
+}
+
+/// Types which can be used as a `slug`
+///
+/// "Slugs" are used to distinguish different pieces of state within an instance.
+/// Typically, each call site that needs to provide an `impl Slug`
+/// will provide a fixed `&'static str`.
+///
+/// Slugs have the same character set restrictions as kinds and instance identities;
+/// see [`InstanceIdentity`].
+/// (This is checked at runtime by the `state_dir` implementation.)
+///
+/// Slugs may not be the same as the reserved device filenames on Windows,
+/// (eg, `con`, `lpr`).
+/// (This is not checked by the `state_dir` implementation,
+/// but violation of this rule will result in code that doesn't work at all on Windows.)
+///
+/// It is important that slugs are distinct within an instance.
+/// Specifically,
+/// each slug provided to a method on the same [`InstanceStateHandle`]
+/// (or a clone of it)
+/// must be different.
+/// Violating this rule does not result in memory-unsafety,
+/// but might result in incorrect operation due to concurrent filesystem access,
+/// including possible data loss and corruption.
+/// (Typically, the slug is fixed, and the [`StorageHandle`]s are usually
+/// obtained during instance construction, so ensuring this is straightforward.)
+// We could implement a runtime check for this by retaining a table of in-use slugs,
+// possibly only with `cfg(debug_assertions)`. However I think this isn't worth the code:
+// it would involve an Arc<Mutex<SlugsInUseTable>> in InstanceStateHnndle and StorageHandle,
+// and Drop impls to remove unused entries (and `raw_subdir` would have imprecise checking
+// unless it returned a Drop newtype around CheckedDir).
+//
+// TODO #1192 for now we are using the name Slug here.
+// When we implement this we may wish to unify parts of the implementation
+// with any general facility that arises from #1192.
+//
+// This is a trait implemented by `str` for convenience of call sites.
+// The implementing Functions here that take slugs will do a runtime syntax check.
+// Doing it this way avoids error handling and newtype boilerplate at call sites,
+// which I think is overkill for an error case that's not at all likely to happen.
+pub trait Slug: ToString {}
+
+impl<T: ToString + ?Sized> Slug for T {}
+
+/// Is an instance still relevant?
+///
+/// Returned by [`InstancePurgeHandler::name_filter`].
+///
+/// See [`StateDirectory::purge_instances`] for details of the semantics.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
+#[allow(clippy::exhaustive_enums)] // this is a boolean
+pub enum Liveness {
+ /// This instance is not known to be interesting
+ ///
+ /// It could be perhaps expired, if it's been long enough
+ PossiblyUnused,
+ /// This instance is still wanted
+ Live,
+}
+
+impl StateDirectory {
+ /// Create a new `StateDirectory` from a directory and mistrust configuration
+ #[allow(clippy::needless_pass_by_value)] // TODO HSS remove
+ pub fn new(state_dir: impl AsRef<Path>, mistrust: Mistrust) -> Result<Self> { todo!() }
+
+ /// Acquires (creates and locks) a storage for an instance
+ ///
+ /// Ensures the existence and suitability of a subdirectory named `kind_identity`,
+ /// and locks it for exclusive access.
+ ///
+ /// `kind` and `identity` have syntactic restrictions -
+ /// see [`InstanceIdString`].
+ pub fn acquire_instance<I: InstanceIdentity>(
+ &self,
+ identity: &I,
+ ) -> Result<InstanceStateHandle> {
+ todo!()
+ }
+
+ /// List the instances of a particular kind
+ ///
+ /// Returns the instance identities.
+ ///
+ /// (The implementation lists subdirectories named `kind_*`.)
+ ///
+ /// Concurrency:
+ /// An instance which is not being removed or created will be
+ /// listed (or not) according to whether it's present.
+ /// But, in the presence of concurrent calls to `acquire_instance` and `delete`
+ /// on different instances,
+ /// is not guaranteed to provide a snapshot:
+ /// serialisation is not guaranteed across different instances.
+ #[allow(clippy::extra_unused_type_parameters)] // TODO HSS remove if possible
+ pub fn list_instances<I: InstanceIdentity>(
+ &self
+ ) -> impl Iterator<Item = Result<InstanceIdString>> {
+ let _: &Void = &self.path;
+ iter::empty()
+ }
+
+ /// Delete instances according to selections made by the caller
+ ///
+ /// Each instance is considered in three stages.
+ ///
+ /// Firstly, it is passed to [`name_filter`](InstancePurgeHandler::name_filter).
+ /// If `name_filter` returns `Live`,
+ /// further consideration is skipped and the instance is retained.
+ ///
+ /// Secondly, the last time the instance was written to is calculated,
+ // This must be done with the lock held, for correctness
+ // but the lock must be acquired in a way that doesn't itself update the modification time.
+ // On Unix this is straightforward because opening for write doesn't update the mtime.
+ // If this is hard on another platform, we'll need a separate stamp file updated
+ // by an explicit Acquire operation.
+ // We should have a test to check that this all works as expected.
+ /// and compared to the return value from
+ /// [`retain_unused_for`](InstancePurgeHandler::retain_unused_for).
+ /// Again, this might mean ensure the instance is retained.
+ ///
+ /// Thirdly, the resulting `InstanceStateHandle` is passed to
+ /// [`dispose`](InstancePurgeHandler::dispose).
+ /// `dispose` may choose to call `handle.delete()`,
+ /// or simply drop the handle.
+ ///
+ /// Concurrency:
+ /// In the presence of multiple concurrent calls to `acquire_instance` and `delete`:
+ /// `filter` may be called for an instance which is being created or deleted
+ /// by another task.
+ /// `dispose` will be properly serialised with other activities on the same instance,
+ /// as implied by it receiving an `InstanceStateHandle`.
+ ///
+ /// Instances which have been acquired
+ /// or modified more recently than `retain_unused_for`
+ /// will not be offered to `dispose`.
+ ///
+ /// The expiry time is reset by calls to `acquire_instance`,
+ /// `StorageHandle::store` and `InstanceStateHandle::raw_subdir`;
+ /// it *may* be reset by calls to `StorageHandle::delete`.
+ pub fn purge_instances<I: InstancePurgeHandler>(
+ &self,
+ filter: &mut I,
+ ) -> Result<()> {
+ todo!()
+ }
+
+ /// Tries to peek at something written by `StorageHandle::store`
+ ///
+ /// It is guaranteed that this will return either the `T` that was stored,
+ /// or `None` if `store` was never called,
+ /// or `StorageHandle::delete` was called
+ ///
+ /// So the operation is atomic, but there is no further synchronisation.
+ //
+ // Not sure if we need this, but it's logically permissible
+ pub fn instance_peek_storage<I: InstanceIdentity, T>(
+ &self,
+ identity: &I,
+ slug: &(impl Slug + ?Sized),
+ ) -> Result<Option<T>> {
+ todo!()
+ }
+}
+
+/// State or cache directory for an instance of a facility
+///
+/// Implies exclusive access:
+/// there is only one `InstanceStateHandle` at a time,
+/// across any number of processes, tasks, and threads,
+/// for the same instance.
+///
+/// But this type is `Clone` and the exclusive access is shared across all clones.
+/// Users of the `InstanceStateHandle` must ensure that functions like
+/// `storage_handle` and `raw_directory` are only called once with each `slug`.
+/// (Typically, the slug is fixed, so this is straightforward.)
+/// See [`Slug`] for more details.
+#[allow(clippy::missing_docs_in_private_items)] // TODO HSS remove
+pub struct InstanceStateHandle {
+ flock_guard: Arc<Todo>,
+}
+
+impl InstanceStateHandle {
+ /// Obtain a [`StorageHandle`], usable for storing/retrieving a `T`
+ ///
+ /// `slug` has syntactic restrictions - see [`InstanceIdString`].
+ pub fn storage_handle<T>(&self, slug: &(impl Slug + ?Sized)) -> Result<StorageHandle<T>> { todo!() }
+
+ /// Obtain a raw filesystem subdirectory, within the directory for this instance
+ ///
+ /// This API is unsuitable platforms without a filesystem accessible via `std::fs`.
+ /// May therefore only be used within Arti for features
+ /// where we're happy to not to support such platforms (eg WASM without WASI)
+ /// without substantial further work.
+ ///
+ /// `slug` has syntactic restrictions - see [`InstanceIdString`].
+ pub fn raw_subdir(&self, slug: &(impl Slug + ?Sized)) -> Result<InstanceRawSubdir> { todo!() }
+
+ /// Unconditionally delete this instance directory
+ ///
+ /// For expiry, use `StateDirectory::purge_instances`,
+ /// and then call this in the `dispose` method.
+ ///
+ /// Will return a `BadAPIUsage` if other clones of this `InstanceStateHandle` exist.
+ pub fn delete(self) -> Result<()> {
+ // use Arc::into_inner on the lock object,
+ // to make sure we're actually the only surviving InstanceStateHandle
+ todo!()
+ }
+}
+
+/// A place in the state or cache directory, where we can load/store a serialisable type
+///
+/// Implies exclusive access.
+///
+/// Rust mutability-xor-sharing rules enforce proper synchronisation,
+/// unless multiple `StorageHandle`s are created
+/// using the same [`InstanceStateHandle`] and `slug`.
+pub struct StorageHandle<T> {
+ /// We're not sync, and we can load and store a `T`
+ marker: PhantomData<Cell<T>>,
+ /// Clone of the InstanceStateHandle's lock
+ flock_guard: Arc<Todo>,
+}
+
+// Like tor_persist, but writing needs `&mut`
+#[allow(missing_docs)] // TODO HSS remove
+impl<T: Serialize + DeserializeOwned> StorageHandle<T> {
+ pub fn delete(&mut self) -> Result<()> {
+ todo!()
+ }
+ pub fn store(&mut self, v: &T) -> Result<()> {
+ todo!()
+ }
+ pub fn load(&self) -> Result<Option<T>> {
+ todo!()
+ }
+}
+
+/// Subdirectory within an instance's state, for raw filesystem operations
+///
+/// Dereferences to `fs_mistrust::CheckedDir` and can be used mostly like one.
+/// Obtained from [`InstanceStateHandle::raw_subdir`].
+///
+/// Existence of this value implies exclusive access to the instance.
+#[derive(Deref, Clone)]
+pub struct InstanceRawSubdir {
+ /// The actual directory, as a [`fs_mistrust::CheckedDir`]
+ #[deref]
+ dir: CheckedDir,
+ /// Clone of the InstanceStateHandle's lock
+ flock_guard: Arc<Todo>,
+}
+
+/// Error accessing persistent state
+#[derive(Error, Clone, Debug)]
+#[non_exhaustive]
+pub enum Error {
+ // will gain variants for:
+ // mistrust error
+ // io::error
+ // serde error
+ // bug
+ //
+ // will contain information such as the fs path or bad parameters
+}
diff --git a/crates/tor-netdoc/src/doc/hsdesc/build.rs b/crates/tor-netdoc/src/doc/hsdesc/build.rs
index f9d06d010..5b2f2a02d 100644
--- a/crates/tor-netdoc/src/doc/hsdesc/build.rs
+++ b/crates/tor-netdoc/src/doc/hsdesc/build.rs
@@ -34,7 +34,7 @@ use super::desc_enc::{HsDescEncNonce, HsDescEncryption, HS_DESC_ENC_NONCE_LEN};
/// This object is constructed via [`HsDescBuilder`], and then turned into a
/// signed document using [`HsDescBuilder::build_sign()`].
///
-/// TODO HSS: Add an example for using this API.
+/// TODO: Add an example for using this API.
#[derive(Builder)]
#[builder(public, derive(Debug, Clone), pattern = "owned", build_fn(vis = ""))]
struct HsDesc<'a> {
diff --git a/crates/tor-netdoc/src/doc/hsdesc/build/inner.rs b/crates/tor-netdoc/src/doc/hsdesc/build/inner.rs
index f4c3178cb..3a029393b 100644
--- a/crates/tor-netdoc/src/doc/hsdesc/build/inner.rs
+++ b/crates/tor-netdoc/src/doc/hsdesc/build/inner.rs
@@ -133,7 +133,7 @@ impl<'a> NetdocBuilder for HsDescInner<'a> {
// "The key is a base64 encoded curve25519 public key used to encrypt the introduction
// request to service. (`KP_hss_ntor`)"
//
- // TODO hss: The spec allows for multiple enc-key lines, but we currently only ever encode
+ // TODO: The spec allows for multiple enc-key lines, but we currently only ever encode
// a single one.
encoder
.item(ENC_KEY)
@@ -145,7 +145,8 @@ impl<'a> NetdocBuilder for HsDescInner<'a> {
// The subject key is the the ed25519 equivalent of the svc_ntor_key curve25519 public
// encryption key.
- // TODO hss: should the sign bit be 0 or 1?
+ // TODO (#1221): this is wrong. The sign bit should be computed according to appendix A
+ // from propo 228.
let signbit = 0;
let ed_svc_ntor_key =
convert_curve25519_to_ed25519_public(&intro_point.svc_ntor_key, signbit)