diff --git a/LICENSE b/LICENSE index d218eff..e952c84 100644 --- a/LICENSE +++ b/LICENSE @@ -1,16 +1,15 @@ -Copyright (c) [2025] [Methanium] +Copyright (c) 2025 Methanium + All rights reserved. -This software is protected by copyright. Copying, editing, -distributing, publicly performing, or any other use of this software -or its components, in source or binary form, is strictly prohibited without the express -written permission of the copyright holder. +No part of this software, source code, documentation, or +associated materials may be copied, reproduced, modified, +distributed, published, sublicensed, sold, or used to create +derivative works without prior written permission from the +copyright holder. -FUTURE LICENSE ACCEPTANCE: -It is the copyright holder's intention to release this software in the future -under a license yet to be defined, which will, among other things, -allow private, non-commercial use. This statement does not constitute -a current license grant and does not alter the above -prohibition on use, copying, or modification. Until the formal -publication of such a future license, all rights remain -reserved. +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY +OF ANY KIND, EXPRESS OR IMPLIED. TO THE MAXIMUM +EXTENT PERMITTED BY LAW, THE COPYRIGHT HOLDER SHALL +NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER +LIABILITY ARISING FROM THE SOFTWARE OR ITS USE. diff --git a/README.md b/README.md index 2f85cf6..aee09ab 100644 --- a/README.md +++ b/README.md @@ -60,4 +60,5 @@ usermod -aG iota-operators USER ``` The user must start a new login session before supplementary group membership -is visible. `IOTA_SOCKET` remains authoritative for custom deployments. +is visible. Unix per-user deployments must set `IOTA_SOCKET` to an absolute +path; Iota does not derive its IPC socket from `XDG_RUNTIME_DIR`. diff --git a/flake.nix b/flake.nix index d7bf620..e2ca8a1 100644 --- a/flake.nix +++ b/flake.nix @@ -42,13 +42,11 @@ commonNativeBuildInputs = with pkgs; [cmake perl pkg-config]; in { packages = { - default = self'.packages.iota-daemon; - - iota-daemon = pkgs.rustPlatform.buildRustPackage { - pname = "iota-daemon"; + default = pkgs.rustPlatform.buildRustPackage { + pname = "iota"; version = "0.1.0"; src = ./.; - cargoBuildFlags = ["-p" "iota-daemon"]; + cargoBuildFlags = ["-p" "iota" "-p" "iota-daemon"]; cargoLock = { lockFile = ./Cargo.lock; allowBuiltinFetchGit = true; @@ -56,6 +54,12 @@ nativeBuildInputs = commonNativeBuildInputs; buildInputs = commonBuildInputs; dontUseCmakeConfigure = true; + passthru.dataDir = "/var/lib/iota"; + }; + + iota-daemon = self'.packages.default.overrideAttrs (old: { + pname = "iota-daemon"; + cargoBuildFlags = ["-p" "iota-daemon"]; postInstall = '' for f in $out/bin/*; do if [ "$(basename "$f")" != "iota-daemon" ]; then @@ -63,33 +67,22 @@ fi done ''; - passthru.dataDir = "/var/lib/iota"; - }; + }); - iota-ui = pkgs.rustPlatform.buildRustPackage { + iota-ui = self'.packages.default.overrideAttrs (old: { pname = "iota-ui"; - version = "0.1.0"; - src = ./.; cargoBuildFlags = ["-p" "iota"]; - cargoLock = { - lockFile = ./Cargo.lock; - allowBuiltinFetchGit = true; - }; - nativeBuildInputs = commonNativeBuildInputs; - buildInputs = commonBuildInputs; - dontUseCmakeConfigure = true; postInstall = '' for f in $out/bin/*; do if [ "$(basename "$f")" != "iota" ]; then rm "$f" fi done - # Rename to avoid confusion if [ -f "$out/bin/iota" ]; then mv "$out/bin/iota" "$out/bin/iota-ui" fi ''; - }; + }); }; devShells.default = pkgs.mkShell { @@ -106,7 +99,7 @@ ... }: let cfg = config.services.iota; - defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.iota-daemon or (throw "iota: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}"); + defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.default or (throw "iota: no pre-built package for system ${pkgs.stdenv.hostPlatform.system}"); configFormat = pkgs.formats.yaml {}; configFile = @@ -190,7 +183,7 @@ users.groups.iota = {}; - systemd.sockets.iota-daemon = { + systemd.sockets.iota = { description = "${descriptionText} IPC socket"; wantedBy = ["sockets.target"]; socketConfig = { @@ -205,10 +198,11 @@ }; }; - systemd.services.iota-daemon = { + systemd.services.iota = { description = descriptionText; - after = ["network.target"]; - requires = ["iota-daemon.socket"]; + wantedBy = ["multi-user.target"]; + after = ["network.target" "iota.socket"]; + requires = ["iota.socket"]; serviceConfig = { diff --git a/iota-installer/src/lib.rs b/iota-installer/src/lib.rs index 3860835..62f663a 100644 --- a/iota-installer/src/lib.rs +++ b/iota-installer/src/lib.rs @@ -172,20 +172,4 @@ fn run(program: &str, args: &[&str]) -> Result<()> { } } -#[cfg(test)] -mod tests { - #[test] - fn service_uses_installed_daemon_and_declared_identities() { - let service = include_str!("../../systemd/iota-daemon.service"); - let socket = include_str!("../../systemd/iota-daemon.socket"); - let sysusers = include_str!("../../systemd/sysusers.d/iota.conf"); - assert!(service.contains("ExecStart=/usr/local/libexec/iota/iota-daemon")); - assert!(service.contains("User=iota")); - assert!(service.contains("Group=iota")); - assert!(socket.contains("SocketUser=iota")); - assert!(socket.contains("SocketGroup=iota-operators")); - assert!(socket.contains("NonBlocking=true")); - assert!(sysusers.contains("u iota ")); - assert!(sysusers.contains("g iota-operators")); - } -} + diff --git a/iota-paths/src/lib.rs b/iota-paths/src/lib.rs index 35a51bd..9143ea1 100644 --- a/iota-paths/src/lib.rs +++ b/iota-paths/src/lib.rs @@ -25,6 +25,7 @@ pub enum IpcEndpoint { #[derive(Debug)] pub enum PathError { MissingPlatformDirectory(&'static str), + MissingRequiredOverride(&'static str), EmptyOverride(&'static str), RelativeOverride { variable: &'static str, @@ -37,6 +38,7 @@ impl fmt::Display for PathError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::MissingPlatformDirectory(name) => write!(f, "missing platform directory: {name}"), + Self::MissingRequiredOverride(name) => write!(f, "{name} must be set"), Self::EmptyOverride(name) => write!(f, "{name} must not be empty"), Self::RelativeOverride { variable, value } => { write!(f, "{variable} must be absolute, got {}", value.display()) @@ -81,7 +83,11 @@ impl IotaPaths { let install_root = override_first(&["IOTA_INSTALL_ROOT"])?.unwrap_or(defaults.install_root); let config_file = override_first(&["IOTA_CONFIG_FILE"])? .unwrap_or_else(|| config_dir.join("config.yaml")); - let ipc_endpoint = resolve_ipc(scope, runtime_dir.as_deref(), defaults.ipc_endpoint)?; + let ipc_endpoint = resolve_ipc(scope, defaults.ipc_endpoint)?; + let runtime_dir = runtime_dir.or_else(|| match &ipc_endpoint { + IpcEndpoint::UnixSocket(path) => path.parent().map(Path::to_path_buf), + IpcEndpoint::WindowsPipe(_) => None, + }); Ok(Self { scope, config_dir, @@ -234,7 +240,7 @@ struct Defaults { log_dir: PathBuf, asset_dir: PathBuf, install_root: PathBuf, - ipc_endpoint: IpcEndpoint, + ipc_endpoint: Option, } impl Defaults { fn for_scope(scope: Scope) -> Result { @@ -250,7 +256,7 @@ impl Defaults { log_dir: "/var/log/iota".into(), asset_dir: "/usr/local/share/iota/web".into(), install_root: "/usr/local/libexec/iota".into(), - ipc_endpoint: IpcEndpoint::UnixSocket("/run/iota/iota.sock".into()), + ipc_endpoint: Some(IpcEndpoint::UnixSocket("/run/iota/iota.sock".into())), }) } #[cfg(not(target_os = "linux"))] @@ -271,18 +277,15 @@ fn user_defaults() -> Result { let state_base = xdg_or_home("XDG_STATE_HOME", &home, ".local/state")?; let cache_base = xdg_or_home("XDG_CACHE_HOME", &home, ".cache")?; let data_base = xdg_or_home("XDG_DATA_HOME", &home, ".local/share")?; - let runtime = absolute_env("XDG_RUNTIME_DIR")? - .ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"))? - .join("iota"); Ok(Defaults { config_dir: config_base.join("iota"), state_dir: state_base.join("iota"), cache_dir: cache_base.join("iota"), - runtime_dir: Some(runtime.clone()), + runtime_dir: None, log_dir: state_base.join("iota/logs"), asset_dir: data_base.join("iota/web"), install_root: data_base.join("iota/bin"), - ipc_endpoint: IpcEndpoint::UnixSocket(runtime.join("iota.sock")), + ipc_endpoint: None, }) } #[cfg(windows)] @@ -301,7 +304,9 @@ fn user_defaults() -> Result { log_dir: local.join("logs"), asset_dir: local.join("data"), install_root: local.join("bin"), - ipc_endpoint: IpcEndpoint::WindowsPipe(r"\\.\pipe\Tensamin.Iota.User".into()), + ipc_endpoint: Some(IpcEndpoint::WindowsPipe( + r"\\.\pipe\Tensamin.Iota.User".into(), + )), }) } @@ -332,20 +337,14 @@ fn override_first(names: &[&'static str]) -> Result, PathError> } Ok(None) } -fn resolve_ipc( - scope: Scope, - runtime: Option<&Path>, - default: IpcEndpoint, -) -> Result { +fn resolve_ipc(scope: Scope, default: Option) -> Result { #[cfg(unix)] { if let Some(path) = absolute_env("IOTA_SOCKET")? { return Ok(IpcEndpoint::UnixSocket(path)); } if scope == Scope::User { - return runtime - .map(|dir| IpcEndpoint::UnixSocket(dir.join("iota.sock"))) - .ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR")); + return Err(PathError::MissingRequiredOverride("IOTA_SOCKET")); } } #[cfg(windows)] @@ -358,7 +357,7 @@ fn resolve_ipc( return Ok(IpcEndpoint::WindowsPipe(name)); } } - Ok(default) + default.ok_or(PathError::UnsupportedScope) } fn create_directory(path: &Path, private: bool) -> std::io::Result<()> { std::fs::create_dir_all(path)?; diff --git a/iota-process-manager/src/lib.rs b/iota-process-manager/src/lib.rs index e3a03d5..c0ebb0d 100644 --- a/iota-process-manager/src/lib.rs +++ b/iota-process-manager/src/lib.rs @@ -218,6 +218,8 @@ mod systemd { pub struct SystemdManager { executor: Arc, + service: &'static str, + socket: &'static str, } impl SystemdManager { pub async fn detect() -> Option { @@ -225,16 +227,29 @@ mod systemd { return None; } let executor: Arc = Arc::new(RealExecutor); - executor + let mut manager = executor .output("systemctl", &["--version", &COMMON[0], &COMMON[1]]) .await .ok() .filter(|r| r.success) - .map(|_| Self { executor }) + .map(|_| Self { + executor, + service: SERVICE, + socket: SOCKET, + })?; + if manager.status("iota.service").await.is_ok() { + manager.service = "iota.service"; + manager.socket = "iota.socket"; + } + Some(manager) } #[cfg(test)] pub fn with_executor(executor: Arc) -> Self { - Self { executor } + Self { + executor, + service: SERVICE, + socket: SOCKET, + } } async fn run(&self, action: &[&str]) -> Result<(), ProcessManagerError> { let mut args = COMMON.to_vec(); @@ -358,19 +373,25 @@ mod systemd { async fn unit_status(&self, unit: &str) -> Result { self.status(unit).await } + async fn iota_startup_status(&self) -> Result { + Ok(DaemonStartupStatus::classify( + self.status(self.service).await?, + self.status(self.socket).await?, + )) + } async fn set_iota_startup_mode( &self, mode: StartupMode, ) -> Result { match mode { StartupMode::AlwaysOn => { - self.run(&["disable", SOCKET]).await?; - self.run(&["enable", "--now", SERVICE]).await?; + self.run(&["disable", self.socket]).await?; + self.run(&["enable", "--now", self.service]).await?; self.verify(DetectedStartupMode::AlwaysOn).await } StartupMode::SocketActivated => { - self.run(&["disable", "--now", SERVICE]).await?; - self.run(&["enable", "--now", SOCKET]).await?; + self.run(&["disable", "--now", self.service]).await?; + self.run(&["enable", "--now", self.socket]).await?; self.verify(DetectedStartupMode::SocketActivated).await } } @@ -378,9 +399,21 @@ mod systemd { async fn unit_action(&self, action: &[&str]) -> Result<(), ProcessManagerError> { self.run(action).await } + async fn process_action( + &self, + action: ProcessAction, + ) -> Result { + let verb = match action { + ProcessAction::Start => "start", + ProcessAction::Stop => "stop", + ProcessAction::Restart => "restart", + }; + self.run(&[verb, self.service]).await?; + self.iota_startup_status().await + } async fn disable_startup(&self) -> Result { - self.run(&["disable", "--now", SERVICE]).await?; - self.run(&["disable", "--now", SOCKET]).await?; + self.run(&["disable", "--now", self.service]).await?; + self.run(&["disable", "--now", self.socket]).await?; self.verify(DetectedStartupMode::Disabled).await } } diff --git a/iota-storage/src/util/e2ee_storage.rs b/iota-storage/src/util/e2ee_storage.rs index ad12661..bfa70c4 100644 --- a/iota-storage/src/util/e2ee_storage.rs +++ b/iota-storage/src/util/e2ee_storage.rs @@ -240,52 +240,4 @@ fn pending_forward_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result String { - let unix = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - format!("{name}-{unix}") - } - - #[test] - fn stores_and_retrieves_chat_secret_blob_for_owner_chat() { - let user_id = suffix("user"); - let chat_id = suffix("chat"); - - put_chat_secret(StoredChatSecret { - user_id: user_id.clone(), - chat_id: chat_id.clone(), - secret_id: "main".to_string(), - version: 1, - encrypted_secret: vec![42, 43], - kem_ciphertext: vec![9, 8, 7], - wrapping_scheme: "mtp-kem-chacha20poly1305-hkdf-sha256-v1".to_string(), - created_at: 1, - updated_at: 2, - }) - .unwrap(); - - let found = get_chat_secret(ChatSecretQuery { - user_id: user_id.clone(), - chat_id: chat_id.clone(), - secret_id: Some("main".to_string()), - }) - .unwrap() - .unwrap(); - assert_eq!(found.encrypted_secret, vec![42, 43]); - assert_eq!(found.kem_ciphertext, vec![9, 8, 7]); - - let denied = get_chat_secret(ChatSecretQuery { - user_id: suffix("other-user"), - chat_id, - secret_id: Some("main".to_string()), - }) - .unwrap(); - assert!(denied.is_none()); - } -}