[Fix] Stability

This commit is contained in:
Alex Emmet 2026-08-28 13:22:59 +02:00
commit 8160f8d0cb
No known key found for this signature in database
44 changed files with 796 additions and 1296 deletions

737
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,53 +9,6 @@ iota-connection = { path = "../iota-connection" }
iota-logger = { path = "../iota-logger" }
iota-util = { path = "../iota-util" }
iota-storage = { path = "../iota-storage" }
iota-state = { path = "../iota-state" }
iota-auth = { path = "../iota-auth" }
actix-web = { version = "4", features = ["rustls-0_23"] }
actix-web-actors = "4"
aes-gcm = "0.10.3"
async-trait = "0.1.89"
base64 = "0.22.1"
chrono = "0.4.43"
crossterm = "*"
dashmap = "6.1.0"
futures = "*"
futures-util = "*"
hex = "*"
hkdf = "0.12.4"
hyper = { version = "1.8.1", features = [
"capi",
"client",
"full",
"http1",
"http2",
"nightly",
"server",
] }
hyper-util = { version = "*" }
json = "*"
lazy_static = "1.5.0"
once_cell = "1.21.3"
open = "5.3.3"
pnet = "0.35.0"
rand = "0.8"
rand_core = { version = "0.6", features = ["getrandom", "std"] }
ratatui = "0.30.0"
reqwest = "0.13.2"
rusqlite = "0.40.0"
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
rustls-pemfile = "2.2.0"
serde_json = "1.0.149"
sha2 = "0.11.0"
strum = "0.28.0"
strum_macros = "0.28.0"
sysinfo = "0.39.0"
tokio = { version = "1.50.0", features = ["full"] }
tokio-tungstenite = { version = "*", features = ["native-tls"] }
tungstenite = "*"
uuid = { version = "*", features = ["v4"] }
walkdir = "2.5.0"
warp = "*"
x448 = { version = "*" }
zip = "6.0.0"

View file

@ -288,8 +288,8 @@ impl ClientConnection {
if cv.is_type(CommunicationType::SettingsSave) {
let my_id = cv.get_sender();
let settings_name = cv.get_data(DataType::SettingsName).as_str().unwrap();
let settings_value = cv.get_data(DataType::Payload).as_str().unwrap();
let Some(settings_name) = cv.get_data(DataType::SettingsName).as_str() else { return };
let Some(settings_value) = cv.get_data(DataType::Payload).as_str() else { return };
let _ = iota_storage::util::settings::save(
my_id as i64,
@ -308,7 +308,7 @@ impl ClientConnection {
if cv.is_type(CommunicationType::SettingsLoad) {
let my_id = cv.get_sender();
let settings_name = cv.get_data(DataType::SettingsName).as_string().unwrap();
let Some(settings_name) = cv.get_data(DataType::SettingsName).as_string() else { return };
let settings_value_str = iota_storage::util::settings::load(
my_id as i64,
iota_storage::util::settings::GLOBAL_SESSION_ID,
@ -350,7 +350,7 @@ impl ClientConnection {
return;
};
let encrypted_challenge = cv.get_data(DataType::Challenge).as_str().unwrap();
let Some(encrypted_challenge) = cv.get_data(DataType::Challenge).as_str() else { return };
let solved = crypto_util::decrypt_challenge(encrypted_challenge, &keyring).ok();

View file

@ -5,56 +5,12 @@ edition = "2024"
[dependencies]
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
iota-logger = { path = "../iota-logger" }
iota-util = { path = "../iota-util" }
iota-storage = { path = "../iota-storage" }
iota-state = { path = "../iota-state" }
iota-auth = { path = "../iota-auth" }
actix-web = { version = "4", features = ["rustls-0_23"] }
actix-web-actors = "4"
aes-gcm = "0.10.3"
async-trait = "0.1.89"
base64 = "0.22.1"
chrono = "0.4.43"
crossterm = "*"
dashmap = "6.1.0"
futures = "*"
futures-util = "*"
hex = "*"
hkdf = "0.12.4"
hyper = { version = "1.8.1", features = [
"capi",
"client",
"full",
"http1",
"http2",
"nightly",
"server",
] }
hyper-util = { version = "*" }
json = "*"
lazy_static = "1.5.0"
once_cell = "1.21.3"
open = "5.3.3"
pnet = "0.35.0"
rand = "0.8"
rand_core = { version = "0.6", features = ["getrandom", "std"] }
ratatui = "0.30.0"
reqwest = "0.13.2"
rusqlite = "0.40.0"
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
rustls-pemfile = "2.2.0"
serde_json = "1.0.149"
sha2 = "0.11.0"
strum = "0.28.0"
strum_macros = "0.28.0"
sysinfo = "0.39.0"
tokio = { version = "1.50.0", features = ["full"] }
tokio-tungstenite = { version = "*", features = ["native-tls"] }
tungstenite = "*"
uuid = { version = "*", features = ["v4"] }
walkdir = "2.5.0"
warp = "*"
x448 = { version = "*" }
zip = "6.0.0"

View file

@ -40,7 +40,7 @@ impl Community {
let mut buf = [0u8; 56];
let mut rng = OsRng;
rng.fill_bytes(&mut buf);
let private_key = Secret::from_bytes(&buf).unwrap();
let private_key = Secret::from(buf);
let public_key = PublicKey::from(&private_key);
Community {
name: String::new(),
@ -58,7 +58,7 @@ impl Community {
let mut buf = [0u8; 56];
let mut rng = OsRng;
rng.fill_bytes(&mut buf);
let private_key = Secret::from_bytes(&buf).unwrap();
let private_key = Secret::from(buf);
let public_key = PublicKey::from(&private_key);
let c = Community {
name,
@ -125,7 +125,7 @@ impl Community {
self.members.clone()
}
pub fn get_private_key(&self) -> Secret {
Secret::from_bytes(self.private_key.as_bytes()).unwrap()
Secret::from(*self.private_key.as_bytes())
}
pub fn get_public_key(&self) -> &PublicKey {
&self.public_key
@ -222,12 +222,14 @@ impl Community {
for interactable in target_interactables.iter() {
if interactable.get_name() == name {
if interactable.get_codec() == "category" {
let category: &Category =
interactable.as_any().downcast_ref::<Category>().unwrap();
let Some(category) = interactable.as_any().downcast_ref::<Category>() else {
return CommunicationValue::new(CommunicationType::ErrorInternal);
};
// cannot move a value of type dyn Interactable the size of dyn Interactable cannot be statically determined (rustc E0161)
return category
.get_child(path.to_string(), name.to_string())
.unwrap()
.ok_or(CommunicationValue::new(CommunicationType::ErrorInternal))
.unwrap_or_else(|error| return error)
.run_function(cv.clone())
.await;
} else {
@ -267,7 +269,7 @@ impl Community {
let mut data = JsonValue::new_object();
let mut permissions = JsonValue::new_array();
for perm in self.permissions.get(user).unwrap() {
for perm in self.permissions.get(user).into_iter().flatten() {
if let Ok(_) = permissions.push(perm.to_string()) {}
}
@ -287,10 +289,10 @@ impl Community {
}
pub async fn load(name: &String) -> Option<Arc<Community>> {
let file_contents = file_util::load_file(&format!("communities/{}/", name), "config.json");
let json_content = json::parse(&file_contents).unwrap();
let json_content = json::parse(&file_contents).ok()?;
let user_data = file_util::load_file(&format!("communities/{}/", name), "users.json");
let user_json: JsonValue = json::parse(&user_data).unwrap();
let user_json: JsonValue = json::parse(&user_data).ok()?;
let mut users = Vec::new();
let mut permissions: HashMap<i64, Vec<Permission>> = HashMap::new();
@ -318,25 +320,17 @@ pub async fn load(name: &String) -> Option<Arc<Community>> {
};
let community = Community {
name: json_content["name"].as_str().unwrap().to_string(),
name: json_content["name"].as_str()?.to_string(),
owner_id: Arc::new(RwLock::new(json_content["owner_id"].as_i64().unwrap_or(0))),
members: users,
roles,
permissions,
private_key: Secret::from_bytes(
&STANDARD
.decode(json_content["private_key"].as_str().unwrap())
.unwrap(),
)
.unwrap(),
public_key: PublicKey::from(
&Secret::from_bytes(
&STANDARD
.decode(json_content["private_key"].as_str().unwrap())
.unwrap(),
)
.unwrap(),
),
&STANDARD.decode(json_content["private_key"].as_str()?).ok()?,
)?,
public_key: PublicKey::from(&Secret::from_bytes(
&STANDARD.decode(json_content["private_key"].as_str()?).ok()?,
)?),
interactables: Arc::new(RwLock::new(Vec::new())),
connections: Arc::new(RwLock::new(HashMap::new())),
};
@ -346,7 +340,7 @@ pub async fn load(name: &String) -> Option<Arc<Community>> {
file_util::get_children(&format!("communities/{}/interactables/", name));
for file in interactable_files {
if file.contains(".json") {
let name = file.split('.').next().unwrap().to_string();
let Some(name) = file.split('.').next().map(str::to_string) else { continue };
let interactable: Box<dyn Interactable> =
registry::load(comarc.clone(), String::new(), name).await;
comarc.add_interactable(Arc::new(interactable)).await;

View file

@ -52,7 +52,9 @@ impl CommunityConnection {
pub async fn send_message(&self, message: &CommunicationValue) {
let mut sender = self.sender.write().await; // Access the SplitSink
let message_text = Message::Text(Utf8Bytes::from(message.to_json().to_string()));
sender.send(message_text).await.unwrap(); // Send the message via the SplitSink
if let Err(error) = sender.send(message_text).await {
log::error!("failed to send community message: {error}");
}
}
pub async fn get_community(&self) -> Option<Arc<Community>> {
self.community.read().await.clone()
@ -94,14 +96,15 @@ impl CommunityConnection {
}
}
async fn handle_function(&self, cv: CommunicationValue) {
let name = cv.get_data(DataType::Name).unwrap().as_str().unwrap();
let path = cv.get_data(DataType::Path).unwrap().as_str().unwrap();
let function = cv.get_data(DataType::Function).unwrap().as_str().unwrap();
let Some(name) = cv.get_data(DataType::Name).as_str() else { return };
let Some(path) = cv.get_data(DataType::Path).as_str() else { return };
let Some(function) = cv.get_data(DataType::Function).as_str() else { return };
let result = self
.get_community()
.await
.unwrap()
.ok_or(())
.unwrap_or_else(|_| return)
.run_function(self.get_user_id().await, name, path, function, &cv)
.await;
@ -364,13 +367,9 @@ impl CommunityConnection {
pub async fn handle_close(self: Arc<Self>) {
if self.is_identified().await {
if self.get_user_id().await != 0 {
self.community
.read()
.await
.as_ref()
.unwrap()
.remove_connection(self.clone())
.await;
if let Some(community) = self.community.read().await.as_ref() {
community.remove_connection(self.clone()).await;
}
}
}
}

View file

@ -30,14 +30,13 @@ impl Category {
.find(|child| child.get_name() == &name)
.cloned()
} else {
let sub_module = path.split("/").next().unwrap();
let sub_module = path.split('/').next()?;
let next = self
.children
.iter()
.find(|child| child.get_name() == sub_module)
.unwrap();
.find(|child| child.get_name() == sub_module)?;
if next.get_codec() == "category" {
let next_cat = next.as_any().downcast_ref::<Category>().unwrap();
let next_cat = next.as_any().downcast_ref::<Category>()?;
next_cat.get_child(path, name)
} else {
Some(next.clone())

View file

@ -4,54 +4,4 @@ version = "0.1.0"
edition = "2024"
[dependencies]
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
iota-logger = { path = "../iota-logger" }
iota-util = { path = "../iota-util" }
iota-storage = { path = "../iota-storage" }
iota-state = { path = "../iota-state" }
aes-gcm = "0.10.3"
async-trait = "0.1.89"
base64 = "0.22.1"
chrono = "0.4.43"
crossterm = "*"
dashmap = "6.1.0"
futures = "*"
futures-util = "*"
hex = "*"
hkdf = "0.12.4"
hyper = { version = "1.8.1", features = [
"capi",
"client",
"full",
"http1",
"http2",
"nightly",
"server",
] }
hyper-util = { version = "*" }
json = "*"
lazy_static = "1.5.0"
once_cell = "1.21.3"
open = "5.3.3"
pnet = "0.35.0"
rand = "0.8"
rand_core = { version = "0.6", features = ["getrandom", "std"] }
ratatui = "0.30.0"
reqwest = "0.13.2"
rusqlite = "0.40.0"
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
rustls-pemfile = "2.2.0"
serde_json = "1.0.149"
sha2 = "0.11.0"
strum = "0.28.0"
strum_macros = "0.28.0"
sysinfo = "0.39.0"
tokio = { version = "1.50.0", features = ["full"] }
tokio-tungstenite = { version = "*", features = ["native-tls"] }
tungstenite = "*"
uuid = { version = "*", features = ["v4"] }
walkdir = "2.5.0"
warp = "*"
x448 = { version = "*" }
zip = "6.0.0"

View file

@ -5,76 +5,24 @@ edition = "2024"
[features]
legacy-commands = [
"dep:iota-logger",
"dep:iota-storage",
"dep:iota-util",
"dep:mtp",
"dep:omikron-connector",
]
[dependencies]
iota-logger = { path = "../iota-logger", optional = true }
iota-state = { path = "../iota-state" }
iota-storage = { path = "../iota-storage", optional = true }
iota-terms = { path = "../iota-terms" }
iota-util = { path = "../iota-util", optional = true }
iota-ipc = { path = "../iota-ipc" }
iota-process-manager = { path = "../iota-process-manager" }
iota-paths = { path = "../iota-paths" }
omikron-connector = { path = "../omikron-connector", optional = true }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", optional = true }
actix-web = { version = "4", features = ["rustls-0_23"] }
actix-web-actors = "4"
aes-gcm = "0.10.3"
async-trait = "0.1.89"
base64 = "0.22.1"
chrono = "0.4.43"
crossterm = "*"
dashmap = "6.1.0"
futures = "*"
futures-util = "*"
hex = "*"
hkdf = "0.12.4"
hyper = { version = "1.8.1", features = [
"capi",
"client",
"full",
"http1",
"http2",
"nightly",
"server",
] }
hyper-util = { version = "*" }
json = "*"
lazy_static = "1.5.0"
once_cell = "1.21.3"
open = "5.3.3"
pnet = "0.35.0"
rand = "0.8"
rand_core = { version = "0.6", features = ["getrandom", "std"] }
ratatui = "0.30.0"
reqwest = "0.13.2"
rusqlite = "0.40.0"
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
rustls-pemfile = "2.2.0"
serde_json = "1.0.149"
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
sha2 = "0.11.0"
strum = "0.28.0"
strum_macros = "0.28.0"
sysinfo = "0.39.0"
tokio = { version = "1.50.0", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }
tokio-tungstenite = { version = "*", features = ["native-tls"] }
tungstenite = "*"
walkdir = "2.5.0"
warp = "*"
x448 = { version = "*" }
zip = "6.0.0"
unicode-width = "0.2"
[dev-dependencies]

View file

@ -94,19 +94,19 @@ impl Screen for TermsCheckerScreen {
height: content_height,
});
let eula_text = if size.width < 70 {
"EULA ¹ (https://legal.tensamin.net/eula/)"
"EULA ¹ (https://legal.methanium.net/tensamin/eula)"
} else {
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/)"
"End User Licence Agreement ¹ (https://legal.methanium.net/tensamin/eula)"
};
let tos_text = if size.width < 72 {
"ToS ² (https://legal.tensamin.net/terms-of-service/)"
"ToS ² (https://legal.methanium.net/tensamin/terms-of-service)"
} else {
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/)"
"Terms of Service ² (https://legal.methanium.net/tensamin/terms-of-service)"
};
let pp_text = if size.width < 68 {
"PP ² (https://legal.tensamin.net/privacy-policy/)"
"PP ² (https://legal.methanium.net/tensamin/privacy-policy)"
} else {
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/)"
"Privacy Policy ² (https://legal.methanium.net/tensamin/privacy-policy)"
};
let (mut optional_lines, agree_lines): (Vec<i16>, Vec<&str>) = if size.width > 143 {

View file

@ -201,7 +201,7 @@ impl Screen for TermsUpdaterScreen {
if self.eula_future {
if size.width < 80 {
text_lines.push(checkbox(
"EULA ¹³ (https://legal.tensamin.net/eula/newest/)",
"EULA ¹³ (https://legal.methanium.net/tensamin/eula)",
self.eula,
self.focus == Focus::Eula,
true,
@ -214,7 +214,7 @@ impl Screen for TermsUpdaterScreen {
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} else {
text_lines.push(checkbox(
"End User Licence Agreement ¹³ (https://legal.tensamin.net/eula/newest/)",
"End User Licence Agreement ¹³ (https://legal.methanium.net/tensamin/eula)",
self.eula,
self.focus == Focus::Eula,
true,
@ -229,14 +229,14 @@ impl Screen for TermsUpdaterScreen {
} else {
if size.width < 80 {
text_lines.push(checkbox(
"EULA ¹ (https://legal.tensamin.net/eula/newest/)",
"EULA ¹ (https://legal.methanium.net/tensamin/eula)",
self.eula,
self.focus == Focus::Eula,
true,
));
} else {
text_lines.push(checkbox(
"End User Licence Agreement ¹ (https://legal.tensamin.net/eula/newest/)",
"End User Licence Agreement ¹ (https://legal.methanium.net/tensamin/eula)",
self.eula,
self.focus == Focus::Eula,
true,
@ -250,7 +250,7 @@ impl Screen for TermsUpdaterScreen {
if self.tos_future {
if size.width < 80 {
text_lines.push(checkbox(
"ToS ²³ (https://legal.tensamin.net/tos/newest/)",
"ToS ²³ (https://legal.methanium.net/tensamin/terms-of-service)",
self.tos,
self.focus == Focus::Tos,
self.eula,
@ -263,7 +263,7 @@ impl Screen for TermsUpdaterScreen {
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} else {
text_lines.push(checkbox(
"Terms of Service ²³ (https://legal.tensamin.net/terms-of-service/newest/)",
"Terms of Service ²³ (https://legal.methanium.net/tensamin/terms-of-service)",
self.tos,
self.focus == Focus::Tos,
self.eula,
@ -278,14 +278,14 @@ impl Screen for TermsUpdaterScreen {
} else {
if size.width < 80 {
text_lines.push(checkbox(
"ToS ² (https://legal.tensamin.net/tos/newest/)",
"ToS ² (https://legal.methanium.net/tensamin/terms-of-service)",
self.tos,
self.focus == Focus::Tos,
self.eula,
));
} else {
text_lines.push(checkbox(
"Terms of Service ² (https://legal.tensamin.net/terms-of-service/newest/)",
"Terms of Service ² (https://legal.methanium.net/tensamin/terms-of-service)",
self.tos,
self.focus == Focus::Tos,
self.eula,
@ -299,7 +299,7 @@ impl Screen for TermsUpdaterScreen {
if self.pp_future {
if size.width < 80 {
text_lines.push(checkbox(
"PP ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
"PP ²³ (https://legal.methanium.net/tensamin/privacy-policy)",
self.pp,
self.focus == Focus::Pp,
self.eula,
@ -312,7 +312,7 @@ impl Screen for TermsUpdaterScreen {
text_lines.push(Line::from(format!(" Goes into effect on {}", date)));
} else {
text_lines.push(checkbox(
"Privacy Policy ²³ (https://legal.tensamin.net/privacy-policy/newest/)",
"Privacy Policy ²³ (https://legal.methanium.net/tensamin/privacy-policy)",
self.pp,
self.focus == Focus::Pp,
self.eula,
@ -327,14 +327,14 @@ impl Screen for TermsUpdaterScreen {
} else {
if size.width < 80 {
text_lines.push(checkbox(
"PP ² (https://legal.tensamin.net/privacy-policy/newest/)",
"PP ² (https://legal.methanium.net/tensamin/privacy-policy)",
self.pp,
self.focus == Focus::Pp,
self.eula,
));
} else {
text_lines.push(checkbox(
"Privacy Policy ² (https://legal.tensamin.net/privacy-policy/newest/)",
"Privacy Policy ² (https://legal.methanium.net/tensamin/privacy-policy)",
self.pp,
self.focus == Focus::Pp,
self.eula,

View file

@ -1,8 +1,9 @@
use iota_util::route_target::RouteTarget;
use mtp::codec::{
CommunicationValue, ProtectionPolicy, RelayError, SignaturePolicy, TypeMap,
CommunicationValue, ProtectionPolicy, RelayError, RelayOpenOptions, SignaturePolicy, TypeMap,
VerifiedRelayContent, VerifiedRelayMetadata, forward_relay_frame,
open_relay_content_with_keyrings, open_relay_metadata_with, relay_metadata_claimed_signer_id,
open_relay_content_with_keyrings, open_relay_metadata_with_without_replay,
relay_metadata_claimed_signer_id,
};
use mtp::crypto::{Keyring, PublicKeyBundle};
use std::fmt;
@ -157,13 +158,12 @@ where
.type_map()
.cloned()
.ok_or(RelayValidationError::MissingTypeMap)?;
let metadata = open_relay_metadata_with(
let metadata = open_relay_metadata_with_without_replay(
frame,
&[keyring],
Some(claimed_signer),
move |signer_id| (signer_id == claimed_signer).then(|| resolver_keys.clone()),
RELAY_PROTECTION_POLICY,
None,
RelayOpenOptions::new(RELAY_PROTECTION_POLICY),
)?;
let context = VerifiedRelayContext {

View file

@ -13,17 +13,6 @@ iota-terms = { path = "../iota-terms" }
iota-updater = { path = "../iota-updater" }
iota-util = { path = "../iota-util" }
iota-paths = { path = "../iota-paths" }
omikron-connector = { path = "../omikron-connector" }
web-server = { path = "../web-server" }
web-ui = { path = "../web-ui" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
dashmap = "6.1.0"
json = "*"
once_cell = "1.21.3"
pnet = "0.35.0"
ratatui = "0.30.0"
reqwest = "0.13.2"
tokio = { version = "1.50.0", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }

View file

@ -2,9 +2,8 @@ use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use iota_cli::screens::terms_checker::{TermsCheckerScreen, UserChoice};
use iota_cli::screens::terms_updater::{TermsUpdaterScreen, UpdateDecision};
use iota_cli::ui::UI;
use iota_terms::{Doc, TermsType as Type, get_current_docs, get_newest_docs};
use iota_terms::{Doc, TermsType as Type, get_current_docs};
use iota_util::file_util::{load_file, save_file};
use tokio::sync::oneshot;
@ -12,9 +11,11 @@ pub async fn check(ui: Arc<UI>) -> Result<(bool, bool), String> {
let mut state = ConsentState::load_state();
ensure_initial_consent(ui.clone(), &mut state).await?;
// A mandatory document update is a hard bootstrap gate. In particular,
// refusing it must not allow service setup or daemon access to continue.
ensure_updates(ui, &mut state).await?;
/*
* The raw legal endpoint exposes only the current document. Restore this
* flow when it provides future versions that users can accept early.
*/
// ensure_updates(ui, &mut state).await?;
state = state.sanitize();
state.save_state();
@ -42,11 +43,9 @@ async fn ensure_initial_consent(ui: Arc<UI>, state: &mut ConsentState) -> Result
return Ok(());
}
let docs = get_current_docs().await;
if docs.is_none() {
return Err("Could not connect to the legal endpoint to fetch the current agreements. Please check your internet connection.".to_string());
}
let (current_eula, current_tos, current_privacy) = docs.unwrap();
let (current_eula, current_tos, current_privacy) = get_current_docs().await.ok_or_else(|| {
"Could not connect to the legal endpoint to fetch the current agreements. Please check your internet connection.".to_string()
})?;
let (tx, rx) = oneshot::channel();
@ -73,6 +72,7 @@ async fn ensure_initial_consent(ui: Arc<UI>, state: &mut ConsentState) -> Result
UserChoice::Deny => Ok(()),
}
}
/*
async fn ensure_updates(ui: Arc<UI>, state: &mut ConsentState) -> Result<(), String> {
let Some((eula_update, tos_update, privacy_update)) = get_updates().await else {
return Ok(());
@ -115,6 +115,8 @@ async fn ensure_updates(ui: Arc<UI>, state: &mut ConsentState) -> Result<(), Str
state.save_state();
Ok(())
}
*/
/*
fn apply_future_updates(
state: &mut ConsentState,
result: UserChoice,
@ -225,6 +227,7 @@ async fn get_updates() -> Option<(
None
}
}
*/
#[derive(Debug, Clone)]
pub struct ConsentState {
@ -299,7 +302,7 @@ impl ConsentState {
if let Some(eula) = &self.eula {
file_out.push_str(&format!("\
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence agreement. You can find our EULA at https://legal.methanium.net/tensamin/eula\
\nEULA={}\
\nEULA-VERSION={}\
\nEULA-HASH={}\
@ -309,7 +312,7 @@ impl ConsentState {
&& let Some(tos) = &self.tos
{
file_out.push_str(&format!("\
\n\"Terms-of-Service=true\" indicates that you read, understood and accepted Tensamin's Terms of Service. You can find our Terms of Serivce at https://legal.tensamin.net/tos/\
\n\"Terms-of-Service=true\" indicates that you read, understood and accepted Tensamin's Terms of Service. You can find our Terms of Serivce at https://legal.methanium.net/tensamin/terms-of-service\
\nTerms-of-Service={}\
\nTerms-of-Service-VERSION={}\
\nTerms-of-Service-HASH={}\
@ -319,7 +322,7 @@ impl ConsentState {
&& let Some(pp) = &self.privacy
{
file_out.push_str(&format!("\
\n\"Privacy-Policy=true\" indicates that you read, understood and accepted Tensamin's Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy/\
\n\"Privacy-Policy=true\" indicates that you read, understood and accepted Tensamin's Privacy Policy. You can find our Privacy Policy at https://legal.methanium.net/tensamin/privacy-policy\
\nPrivacy-Policy={}\
\nPrivacy-Policy-VERSION={}\
\nPrivacy-Policy-HASH={}\
@ -327,7 +330,7 @@ impl ConsentState {
}
} else {
file_out.push_str("\
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.tensamin.net/eula/\
\n\"EULA=true\" indicates that you read, understood and accepted Tensamin's End User Licence Agreement. You can find Tensamin's EULA at https://legal.methanium.net/tensamin/eula\
\nEULA=false\
");
}

View file

@ -13,9 +13,8 @@ iota-updater = { path = "../iota-updater" }
iota-util = { path = "../iota-util" }
omikron-connector = { path = "../omikron-connector" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
dashmap = "6.1.0"
libc = "0.2"
sysinfo = "0.39.0"
sysinfo = "0.38.0"
serde_yaml = "0.9"
tokio = { version = "1.50.0", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }

View file

@ -7,7 +7,6 @@ edition = "2024"
iota-daemon-lib = { path = "../iota-daemon-lib" }
iota-ipc = { path = "../iota-ipc" }
iota-logger = { path = "../iota-logger" }
iota-state = { path = "../iota-state" }
iota-paths = { path = "../iota-paths" }
iota-storage = { path = "../iota-storage" }
iota-util = { path = "../iota-util" }
@ -15,4 +14,3 @@ iota-terms = { path = "../iota-terms" }
omikron-connector = { path = "../omikron-connector" }
web-server = { path = "../web-server" }
tokio = { version = "1.50.0", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }

View file

@ -102,15 +102,18 @@ async fn main() -> ExitCode {
let (state_tx, state_rx) = watch::channel(runtime.snapshot());
runtime.set_startup_phase(StartupPhase::LoadingUsers);
if tokio::task::spawn_blocking(user_manager::load_users_sync)
let storage_error = match iota_storage::util::db::verify_and_backup_database() {
Ok(()) => tokio::task::spawn_blocking(user_manager::load_users_sync)
.await
.ok()
.and_then(Result::ok)
.is_none()
{
.map_err(|error| format!("user storage task failed: {error}"))
.and_then(|result| result.map_err(|error| error.to_string()))
.err(),
Err(error) => Some(error.to_string()),
};
if let Some(error) = storage_error {
runtime.set_component_failed(
iota_ipc::ComponentId::Storage,
"user storage failed to load".into(),
format!("user storage failed to load: {error}"),
);
} else {
runtime.set_component_healthy(iota_ipc::ComponentId::Storage, None);

View file

@ -19,6 +19,10 @@ pub fn install_linux_bundle(bundle: &Path) -> Result<()> {
install_linux_bundle_with_operator(bundle, None)
}
pub fn bootstrap_linux_bundle(bundle: &Path, operator: Option<&str>) -> Result<()> {
install_linux_bundle_with_operator(bundle, operator)
}
pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>) -> Result<()> {
if std::env::consts::OS != "linux" {
bail!("Linux systemd bundles are not supported on this platform");
@ -118,6 +122,12 @@ pub fn install_linux_bundle_with_operator(bundle: &Path, operator: Option<&str>)
],
)?;
run("systemd-sysusers", &[])?;
for directory in ["/var/lib/iota", "/var/cache/iota", "/var/log/iota"] {
run(
"install",
&["-d", "-m", "0750", "-o", "iota", "-g", "iota", directory],
)?;
}
if let Some(operator) = operator {
run("usermod", &["-aG", "iota-operators", operator])?;
} else {

View file

@ -15,6 +15,7 @@ pub const COMMANDS: &[&str] = &[
"config get",
"config set ",
"config reload",
"health",
"components",
"logs",
"update check",
@ -84,6 +85,7 @@ pub fn parse(line: &str) -> Option<LocalRequest> {
}),
["config", "reload"] => Some(LocalRequest::ReloadConfig),
["omikron", "status"] => Some(LocalRequest::GetOmikronStatus),
["health"] => Some(LocalRequest::ListComponents),
["components"] => Some(LocalRequest::ListComponents),
["logs"] => Some(LocalRequest::GetLogs { limit: 100 }),
["update", "check"] => Some(LocalRequest::CheckUpdate),
@ -229,6 +231,14 @@ mod tests {
));
}
#[test]
fn parses_health() {
assert!(matches!(
parse("health"),
Some(LocalRequest::ListComponents)
));
}
#[test]
fn parses_users_show() {
let req = parse("users show 42").unwrap();

View file

@ -355,7 +355,7 @@ fn format_data_container(data: Vec<(DataTypeId, DataValue)>, version: Version) -
let key_str = key.to_string();
match value {
DataValue::Str(s) => format!("{}=\"{}\"", key_str, s),
DataValue::Str(s) => format!("{}=\"{}\"", key_str, abbreviate_string(&s)),
DataValue::Container(inner) => {
let inner_formatted = format_data_container(inner, version.clone());
@ -386,7 +386,7 @@ fn format_array(arr: Vec<DataValue>, version: Version) -> String {
let parts: Vec<String> = arr
.into_iter()
.map(|value| match value {
DataValue::Str(s) => format!("\"{}\"", s),
DataValue::Str(s) => format!("\"{}\"", abbreviate_string(&s)),
DataValue::Container(inner) => {
let inner_formatted = format_data_container(inner, version.clone());
@ -412,6 +412,31 @@ fn format_array(arr: Vec<DataValue>, version: Version) -> String {
parts.join(", ")
}
fn abbreviate_string(value: &str) -> String {
const EDGE_LENGTH: usize = 4;
let chars: Vec<char> = value.chars().collect();
if chars.len() <= EDGE_LENGTH * 2 {
return value.to_string();
}
let prefix: String = chars.iter().take(EDGE_LENGTH).collect();
let suffix: String = chars.iter().rev().take(EDGE_LENGTH).rev().collect();
format!("{prefix}...{suffix}")
}
#[cfg(test)]
mod tests {
use super::abbreviate_string;
#[test]
fn abbreviates_only_strings_longer_than_eight_characters() {
assert_eq!(abbreviate_string("12345678"), "12345678");
assert_eq!(abbreviate_string("123456789"), "1234...6789");
assert_eq!(abbreviate_string("YWJjZGVmZ2hpag=="), "YWJj...ag==");
}
}
#[macro_export]
macro_rules! log_cv {
($kind:expr, $cv:expr) => {

View file

@ -70,20 +70,37 @@ pub struct IotaPaths {
impl IotaPaths {
pub fn resolve(scope: Scope) -> Result<Self, PathError> {
let defaults = Defaults::for_scope(scope)?;
let config_dir = override_first(&["IOTA_CONFIG_DIR"])?.unwrap_or(defaults.config_dir);
let data_root = if scope == Scope::User {
absolute_env("IOTA_DATA_ROOT")?
} else {
None
};
let config_dir = override_first(&["IOTA_CONFIG_DIR"])?
.or_else(|| data_root.as_ref().map(|root| root.join("config")))
.unwrap_or(defaults.config_dir);
// IOTA_DATA_DIR is intentionally only a compatibility alias. Parse it
// exactly like every other override; do not hide an invalid value.
let state_dir =
override_first(&["IOTA_STATE_DIR", "IOTA_DATA_DIR"])?.unwrap_or(defaults.state_dir);
let cache_dir = override_first(&["IOTA_CACHE_DIR"])?.unwrap_or(defaults.cache_dir);
let runtime_dir = override_first(&["IOTA_RUNTIME_DIR"])?.or(defaults.runtime_dir);
let log_dir = override_first(&["IOTA_LOG_DIR"])?.unwrap_or(defaults.log_dir);
let state_dir = override_first(&["IOTA_STATE_DIR", "IOTA_DATA_DIR"])?
.or_else(|| data_root.as_ref().map(|root| root.join("state")))
.unwrap_or(defaults.state_dir);
let cache_dir = override_first(&["IOTA_CACHE_DIR"])?
.or_else(|| data_root.as_ref().map(|root| root.join("cache")))
.unwrap_or(defaults.cache_dir);
let runtime_dir = override_first(&["IOTA_RUNTIME_DIR"])?
.or_else(|| data_root.as_ref().map(|root| root.join("runtime")))
.or(defaults.runtime_dir);
let log_dir = override_first(&["IOTA_LOG_DIR"])?
.or_else(|| data_root.as_ref().map(|root| root.join("logs")))
.unwrap_or(defaults.log_dir);
let asset_dir = override_first(&["IOTA_ASSET_DIR", "IOTA_WEB_ASSET_DIR"])?
.or_else(|| data_root.as_ref().map(|root| root.join("web")))
.unwrap_or(defaults.asset_dir);
let install_root = override_first(&["IOTA_INSTALL_ROOT"])?.unwrap_or(defaults.install_root);
let install_root = override_first(&["IOTA_INSTALL_ROOT"])?
.or_else(|| data_root.as_ref().map(|root| root.join("bin")))
.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, defaults.ipc_endpoint)?;
let ipc_endpoint = resolve_ipc(scope, defaults.ipc_endpoint, data_root.as_deref())?;
let runtime_dir = runtime_dir.or_else(|| match &ipc_endpoint {
IpcEndpoint::UnixSocket(path) => path.parent().map(Path::to_path_buf),
IpcEndpoint::WindowsPipe(_) => None,
@ -136,10 +153,20 @@ impl IotaPaths {
&self.cache_dir,
&self.log_dir,
] {
create_directory(directory, self.scope == Scope::User)?;
create_directory(directory, self.scope == Scope::User).map_err(|error| {
std::io::Error::new(
error.kind(),
format!("cannot prepare {}: {error}", directory.display()),
)
})?;
}
if let Some(runtime) = &self.runtime_dir {
create_directory(runtime, self.scope == Scope::User)?;
create_directory(runtime, self.scope == Scope::User).map_err(|error| {
std::io::Error::new(
error.kind(),
format!("cannot prepare {}: {error}", runtime.display()),
)
})?;
}
Ok(())
}
@ -337,12 +364,19 @@ fn override_first(names: &[&'static str]) -> Result<Option<PathBuf>, PathError>
}
Ok(None)
}
fn resolve_ipc(scope: Scope, default: Option<IpcEndpoint>) -> Result<IpcEndpoint, PathError> {
fn resolve_ipc(
scope: Scope,
default: Option<IpcEndpoint>,
data_root: Option<&Path>,
) -> Result<IpcEndpoint, PathError> {
#[cfg(unix)]
{
if let Some(path) = absolute_env("IOTA_SOCKET")? {
return Ok(IpcEndpoint::UnixSocket(path));
}
if let Some(root) = data_root {
return Ok(IpcEndpoint::UnixSocket(root.join("runtime/iota.sock")));
}
if scope == Scope::User {
return Err(PathError::MissingRequiredOverride("IOTA_SOCKET"));
}
@ -451,9 +485,13 @@ pub fn daemon_endpoints() -> Vec<PathBuf> {
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{
Mutex,
atomic::{AtomicU64, Ordering},
};
static TEST_ID: AtomicU64 = AtomicU64::new(0);
static ENVIRONMENT: Mutex<()> = Mutex::new(());
#[test]
fn system_layout_is_fhs() {
let p = IotaPaths::resolve(Scope::System).unwrap();
@ -501,4 +539,31 @@ mod tests {
assert!(state.join("path-layout-v2.json").is_file());
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn data_root_keeps_unmanaged_user_paths_together() {
let _guard = ENVIRONMENT.lock().unwrap();
let root = std::env::temp_dir().join(format!(
"iota-data-root-test-{}-{}",
std::process::id(),
TEST_ID.fetch_add(1, Ordering::Relaxed)
));
unsafe {
std::env::set_var("IOTA_DATA_ROOT", &root);
}
let paths = IotaPaths::resolve(Scope::User).unwrap();
unsafe {
std::env::remove_var("IOTA_DATA_ROOT");
}
assert_eq!(paths.config_file, root.join("config/config.yaml"));
assert_eq!(paths.state_dir, root.join("state"));
assert_eq!(paths.cache_dir, root.join("cache"));
assert_eq!(paths.log_dir, root.join("logs"));
assert_eq!(paths.runtime_dir, Some(root.join("runtime")));
assert_eq!(
paths.ipc_endpoint,
IpcEndpoint::UnixSocket(root.join("runtime/iota.sock"))
);
}
}

View file

@ -12,6 +12,4 @@ dashmap = "6.1.0"
once_cell = "1.21.3"
tokio = { version = "1.50.0", features = ["full"] }
json = "*"
sysinfo = "0.39.0"
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
serde = { version = "1", features = ["derive"] }
sysinfo = "0.38.0"

View file

@ -5,33 +5,17 @@ edition = "2024"
[dependencies]
iota-logger = { path = "../iota-logger" }
iota-state = { path = "../iota-state" }
iota-util = { path = "../iota-util" }
iota-paths = { path = "../iota-paths" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
aes-gcm = "0.10.3"
base64 = "0.22.1"
hex = "*"
hkdf = "0.12.4"
json = "*"
arc-swap = "1"
once_cell = "1.21.3"
r2d2 = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
thiserror = "2"
rand = "0.8"
rand_core = { version = "0.6", features = ["getrandom", "std"] }
ratatui = "0.30.0"
reqwest = "0.13.2"
rusqlite = "0.40.0"
sha2 = "0.11.0"
sysinfo = "0.39.0"
tokio = { version = "1.50.0", features = ["full"] }
uuid = { version = "*", features = ["v4"] }
walkdir = "2.5.0"
x448 = { version = "*" }
zip = "6.0.0"

View file

@ -149,10 +149,8 @@ pub fn save_config_to(path: &Path) {
return;
}
}
let temporary = path.with_extension("yaml.tmp");
if let Err(error) = fs::write(&temporary, yaml).and_then(|_| fs::rename(&temporary, path)) {
if let Err(error) = iota_util::atomic_file::replace(path, yaml.as_bytes(), 3) {
eprintln!("Cannot save {}: {error}", path.display());
let _ = fs::remove_file(temporary);
}
}
}

View file

@ -19,7 +19,7 @@ impl ManageConnection for SqliteManager {
fn connect(&self) -> Result<Connection, rusqlite::Error> {
let path = db_file_path(DB_NAME);
let conn = Connection::open(path)?;
conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;")?;
conn.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;")?;
conn.busy_timeout(Duration::from_millis(250))?;
Ok(conn)
}
@ -55,6 +55,52 @@ where
f(&conn)
}
/* Verify the persistent database before the pool is initialized. A corrupt
* database is moved aside rather than opened again, preserving material for
* operator recovery while allowing the daemon to report the failed storage. */
pub fn verify_and_backup_database() -> Result<(), StorageError> {
let storage_dir = iota_util::file_util::storage_directory();
std::fs::create_dir_all(&storage_dir)?;
let path = storage_dir.join(format!("{DB_NAME}.sqlite3"));
if !path.exists() {
return Ok(());
}
let connection = Connection::open(&path)?;
connection.execute_batch("PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;")?;
connection.execute_batch("PRAGMA wal_checkpoint(FULL);")?;
let integrity: String = connection.query_row("PRAGMA integrity_check", [], |row| row.get(0))?;
drop(connection);
if integrity != "ok" {
let recovery = storage_dir.join("recovery");
std::fs::create_dir_all(&recovery)?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
for suffix in ["", "-wal", "-shm"] {
let source = PathBuf::from(format!("{}{}", path.display(), suffix));
if source.exists() {
let destination = recovery.join(format!("{DB_NAME}.sqlite3.{timestamp}{suffix}"));
std::fs::rename(source, destination)?;
}
}
return Err(StorageError::Other(format!(
"database integrity check failed ({integrity}); moved database files to {}",
recovery.display()
)));
}
let backup_dir = storage_dir.join("backups");
std::fs::create_dir_all(&backup_dir)?;
let backup = backup_dir.join(format!("{DB_NAME}.sqlite3"));
let temporary = backup_dir.join(format!(".{DB_NAME}.sqlite3.tmp"));
std::fs::copy(&path, &temporary)?;
std::fs::File::open(&temporary)?.sync_all()?;
std::fs::rename(temporary, backup)?;
Ok(())
}
fn db_file_path(db_name: &str) -> PathBuf {
let storage_dir = iota_util::file_util::storage_directory();
// Creating storage belongs to initialization/connection setup, never to a

View file

@ -4,8 +4,7 @@ version = "0.1.0"
edition = "2024"
[dependencies]
iota-state = { path = "../iota-state" }
iota-util = { path = "../iota-util" }
json = "*"
reqwest = "0.13.2"
tokio = { version = "1.50.0", features = ["macros"] }

View file

@ -40,7 +40,7 @@ impl ConsentRecord {
}
fn matches_doc(value: &Option<(String, String)>, doc: &Doc) -> bool {
matches!(value, Some((version, hash)) if version == &doc.get_version() && hash == &doc.get_hash())
matches!(value, Some((_, hash)) if hash == &doc.get_hash())
}
pub fn load(state_dir: &Path) -> ConsentRecord {
@ -82,8 +82,5 @@ pub fn save(state_dir: &Path, record: &ConsentRecord) -> io::Result<()> {
text.push_str(&format!("{name}={version}:{hash}\n"));
}
}
let path = state_dir.join(FILE_NAME);
let temporary = state_dir.join(format!(".{FILE_NAME}.{}.tmp", std::process::id()));
fs::write(&temporary, text)?;
fs::rename(temporary, path)
iota_util::atomic_file::replace(&state_dir.join(FILE_NAME), text.as_bytes(), 3)
}

View file

@ -1,7 +1,5 @@
use json::{JsonValue, object::Object};
use crate::terms_getter::Type;
use iota_util::file_util::load_file;
use iota_util::crypto_helper::hex_hash;
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(unused)]
@ -23,15 +21,24 @@ impl Doc {
}
}
pub fn from_raw(doc_type: Type, content: String, timestamp: u64) -> Doc {
Doc::new(
timestamp.to_string(),
hex_hash(&content),
doc_type,
timestamp,
)
}
pub fn equals_some(&self, other: &Option<Self>) -> bool {
if let Some(other) = other {
self.get_version() == other.get_version() && self.get_hash() == other.get_hash()
self.equals(other)
} else {
false
}
}
pub fn equals(&self, other: &Self) -> bool {
self.get_version() == other.get_version() && self.get_hash() == other.get_hash()
self.get_hash() == other.get_hash()
}
pub fn get_version(&self) -> String {
@ -41,34 +48,36 @@ impl Doc {
self.hash.clone()
}
pub fn get_time(&self) -> u64 {
self.timestamp.clone()
self.timestamp
}
pub fn get_content(&self) -> String {
load_file(
format!("docs/{}/", self.doc_type.to_str()).as_str(),
format!("{}.md", self.version).as_str(),
)
#[cfg(test)]
fn timestamp(&self) -> u64 {
self.timestamp
}
}
pub fn to_json(&self) -> JsonValue {
let mut json = JsonValue::new_object();
#[cfg(test)]
mod tests {
use super::Doc;
use crate::terms_getter::Type;
use iota_util::crypto_helper::hex_hash;
let _ = json.insert("version", self.version.clone());
let _ = json.insert("hash", self.hash.clone());
let _ = json.insert("unix", self.timestamp.clone());
#[test]
fn raw_documents_use_a_local_timestamp_and_content_hash() {
let content = "# EULA\n".to_owned();
let document = Doc::from_raw(Type::EULA, content.clone(), 123);
json
assert_eq!(document.doc_type, Type::EULA);
assert_eq!(document.get_version(), "123");
assert_eq!(document.timestamp(), 123);
assert_eq!(document.get_hash(), hex_hash(&content));
}
pub fn from_json(doc_type: Type, json: Object) -> Option<Self> {
let hash = json.get("hash")?.as_str()?.to_string();
let version = json.get("version")?.as_str()?.to_string();
let timestamp = json.get("unix")?.as_u64()?;
Some(Doc {
version,
hash,
doc_type,
timestamp,
})
#[test]
fn matching_documents_ignore_the_fetch_timestamp() {
let earlier = Doc::from_raw(Type::EULA, "# EULA\n".to_owned(), 123);
let later = Doc::from_raw(Type::EULA, "# EULA\n".to_owned(), 456);
assert!(earlier.equals(&later));
}
}

View file

@ -4,7 +4,7 @@ pub mod terms_getter;
pub use terms_getter::Type as TermsType;
pub use terms_getter::get_current_docs;
pub use terms_getter::get_link;
pub use terms_getter::get_newest_docs;
// pub use terms_getter::get_newest_docs;
pub use terms_getter::get_newest_link;
pub use terms_getter::get_terms;

View file

@ -1,6 +1,5 @@
use json::JsonValue::Object;
use crate::doc::Doc;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Type {
@ -13,8 +12,8 @@ impl Type {
pub fn to_str(&self) -> &str {
match self {
Self::EULA => "eula",
Self::TOS => "tos",
Self::PP => "privacy",
Self::TOS => "terms-of-service",
Self::PP => "privacy-policy",
}
}
pub fn to_string(&self) -> String {
@ -27,79 +26,73 @@ impl Type {
}
pub fn get_link(terms_type: Type) -> String {
format!("https://legal.tensamin.net/{}/", terms_type.to_str())
format!(
"https://legal.methanium.net/tensamin/{}",
terms_type.to_str()
)
}
/*
* The legal service exposes only its latest raw documents. Keep this helper
* for the dormant pre-emptive-acceptance UI until it has a source of future
* document versions again.
*/
pub fn get_newest_link(terms_type: Type) -> String {
format!("https://legal.tensamin.net/{}/newest/", terms_type.to_str())
get_link(terms_type)
}
pub async fn get_current_docs() -> Option<(Doc, Doc, Doc)> {
let body = reqwest::get("https://legal.tensamin.net/api/current/")
.await
.ok()?
.text()
.await
.ok()?;
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs();
let (eula, tos, privacy) = tokio::join!(
get_terms(Type::EULA),
get_terms(Type::TOS),
get_terms(Type::PP),
);
let json = json::parse(&body).ok()?;
if let Object(eula) = &json["eula"] {
if let Object(tos) = &json["tos"] {
if let Object(pp) = &json["pp"] {
Some((
Doc::from_json(Type::EULA, eula.clone())?,
Doc::from_json(Type::TOS, tos.clone())?,
Doc::from_json(Type::PP, pp.clone())?,
Doc::from_raw(Type::EULA, eula?, timestamp),
Doc::from_raw(Type::TOS, tos?, timestamp),
Doc::from_raw(Type::PP, privacy?, timestamp),
))
} else {
None
}
} else {
None
}
} else {
None
}
}
/*
* Future documents are unavailable from the raw endpoint. Restore this API
* with the pre-emptive-acceptance flow when the service provides them again.
*
pub async fn get_newest_docs() -> Option<(Doc, Doc, Doc)> {
let body = reqwest::get("https://legal.tensamin.net/api/newest/")
.await
.ok()?
.text()
.await
.ok()?;
let json = json::parse(&body).ok()?;
if let Object(eula) = &json["eula"] {
if let Object(tos) = &json["tos"] {
if let Object(pp) = &json["pp"] {
Some((
Doc::from_json(Type::EULA, eula.clone())?,
Doc::from_json(Type::TOS, tos.clone())?,
Doc::from_json(Type::PP, pp.clone())?,
))
} else {
None
}
} else {
None
}
} else {
None
}
}
*/
pub async fn get_terms(terms_type: Type) -> Option<String> {
let body = reqwest::get(format!(
"https://legal.tensamin.net/api/text/{}/",
reqwest::get(format!(
"https://legal.methanium.net/tensamin/{}/raw",
terms_type.to_str()
))
.await
.ok()?
.text()
.await
.ok()?;
.ok()
}
Some(body)
#[cfg(test)]
mod tests {
use super::{Type, get_link};
#[test]
fn maps_document_types_to_tensamin_raw_document_names() {
assert_eq!(Type::EULA.to_str(), "eula");
assert_eq!(Type::TOS.to_str(), "terms-of-service");
assert_eq!(Type::PP.to_str(), "privacy-policy");
}
#[test]
fn links_to_the_tensamin_document_page() {
assert_eq!(
get_link(Type::TOS),
"https://legal.methanium.net/tensamin/terms-of-service"
);
}
}

View file

@ -4,31 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
iota-logger = { path = "../iota-logger" }
iota-paths = { path = "../iota-paths" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
json = "*"
pnet = "0.35.0"
ratatui = "0.30.0"
reqwest = "0.13.2"
tokio = { version = "1.50.0", features = ["full"] }
sysinfo = "0.39.0"
uuid = { version = "*", features = ["v4"] }
walkdir = "2.5.0"
zip = "6.0.0"
aes-gcm = "0.10.3"
base64 = "0.22.1"
rand_core = { version = "0.6", features = ["getrandom", "std"] }
sha2 = "0.11.0"
x448 = { version = "*" }
hkdf = "0.12.4"
once_cell = "1.21.3"
hex = "*"
serde = "1.0.228"
tempfile = "3.27.0"
anyhow = "1.0.102"
semver = "1.0.28"
ed25519-dalek = "2.2.0"
serde_json = "1.0"

View file

@ -11,9 +11,12 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
reqwest = "0.13.2"
tokio = { version = "1.50.0", features = ["full"] }
sysinfo = "0.39.0"
sysinfo = "0.38.0"
uuid = { version = "*", features = ["v4"] }
walkdir = "2.5.0"
zip = "6.0.0"
base64 = "0.22.1"
hex = "*"
[dev-dependencies]
tempfile = "3"

View file

@ -0,0 +1,158 @@
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static TEMPORARY_ID: AtomicU64 = AtomicU64::new(0);
/* Persist small state files without exposing a partially written version after
* a crash. Backups give operators a local recovery point for keys and config. */
pub fn replace(path: &Path, contents: &[u8], backup_limit: usize) -> io::Result<()> {
replace_with_mode(path, contents, backup_limit, false)
}
pub fn replace_private(path: &Path, contents: &[u8], backup_limit: usize) -> io::Result<()> {
replace_with_mode(path, contents, backup_limit, true)
}
fn replace_with_mode(
path: &Path,
contents: &[u8],
backup_limit: usize,
private: bool,
) -> io::Result<()> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"persistent file has no parent directory",
)
})?;
fs::create_dir_all(parent)?;
if backup_limit > 0 && path.is_file() {
create_backup(path, backup_limit)?;
}
let temporary = temporary_path(path)?;
let write_result = (|| {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&temporary)?;
set_private_permissions(&temporary, private)?;
file.write_all(contents)?;
file.sync_all()?;
fs::rename(&temporary, path)?;
sync_directory(parent)
})();
if write_result.is_err() {
let _ = fs::remove_file(&temporary);
}
write_result
}
#[cfg(unix)]
fn set_private_permissions(path: &Path, private: bool) -> io::Result<()> {
if private {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
}
Ok(())
}
#[cfg(not(unix))]
fn set_private_permissions(_path: &Path, _private: bool) -> io::Result<()> {
Ok(())
}
fn create_backup(path: &Path, backup_limit: usize) -> io::Result<()> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"persistent file has no parent directory",
)
})?;
let name = path.file_name().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "persistent file has no name")
})?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let id = TEMPORARY_ID.fetch_add(1, Ordering::Relaxed);
let backup = parent.join(format!(
".{}.backup-{timestamp}-{id}",
name.to_string_lossy()
));
fs::copy(path, &backup)?;
File::open(&backup)?.sync_all()?;
sync_directory(parent)?;
let prefix = format!(".{}.backup-", name.to_string_lossy());
let mut backups = fs::read_dir(parent)?
.filter_map(Result::ok)
.filter(|entry| entry.file_name().to_string_lossy().starts_with(&prefix))
.collect::<Vec<_>>();
backups.sort_by_key(|entry| entry.file_name());
let obsolete = backups.len().saturating_sub(backup_limit);
for entry in backups.into_iter().take(obsolete) {
fs::remove_file(entry.path())?;
}
Ok(())
}
fn temporary_path(path: &Path) -> io::Result<PathBuf> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"persistent file has no parent directory",
)
})?;
let name = path.file_name().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "persistent file has no name")
})?;
let id = TEMPORARY_ID.fetch_add(1, Ordering::Relaxed);
Ok(parent.join(format!(
".{}.{}.{}.tmp",
name.to_string_lossy(),
std::process::id(),
id
)))
}
#[cfg(unix)]
fn sync_directory(path: &Path) -> io::Result<()> {
File::open(path)?.sync_all()
}
#[cfg(not(unix))]
fn sync_directory(_path: &Path) -> io::Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::replace;
#[test]
fn replace_preserves_a_previous_version_as_a_backup() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("state");
replace(&path, b"first", 2).unwrap();
replace(&path, b"second", 2).unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"second");
let backups = std::fs::read_dir(directory.path())
.unwrap()
.filter_map(Result::ok)
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.contains(".state.backup-")
})
.count();
assert_eq!(backups, 1);
}
}

View file

@ -6,7 +6,10 @@ pub fn generate_keyring() -> Keyring {
}
pub fn keyring_to_base64(keyring: &Keyring) -> String {
STANDARD.encode(keyring.to_bytes())
keyring
.try_to_bytes()
.map(|bytes| STANDARD.encode(bytes))
.unwrap_or_default()
}
pub fn keyring_from_base64(s: &str) -> Option<Keyring> {
@ -15,7 +18,10 @@ pub fn keyring_from_base64(s: &str) -> Option<Keyring> {
}
pub fn public_key_bundle_to_base64(bundle: &PublicKeyBundle) -> String {
STANDARD.encode(bundle.as_bytes())
bundle
.try_as_bytes()
.map(|bytes| STANDARD.encode(bytes))
.unwrap_or_default()
}
pub fn public_key_bundle_from_base64(s: &str) -> Option<PublicKeyBundle> {

View file

@ -1,3 +1,4 @@
pub mod atomic_file;
pub mod crypto_helper;
pub mod crypto_util;
pub mod file_util;

View file

@ -13,7 +13,6 @@ iota-paths = { path = "../iota-paths" }
iota-terms = { path = "../iota-terms" }
iota-util = { path = "../iota-util" }
tokio = { version = "1.50.0", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }
serde_json = "1"
serde_yaml = "0.9"
clap = { version = "4.5", features = ["derive"] }

View file

@ -93,6 +93,7 @@ enum CliCommand {
yes: bool,
},
Components,
Health,
Logs {
#[arg(long, default_value_t = 100)]
limit: usize,
@ -209,6 +210,12 @@ enum DaemonAction {
#[arg(long)]
operator: Option<String>,
},
Bootstrap {
#[arg(long)]
bundle: String,
#[arg(long)]
operator: Option<String>,
},
}
#[derive(Args, Debug)]
struct UpdateArgs {
@ -276,6 +283,10 @@ pub enum Command {
bundle: String,
operator: Option<String>,
},
Bootstrap {
bundle: String,
operator: Option<String>,
},
Status,
Tasks,
UsersList,
@ -329,6 +340,7 @@ pub enum Command {
confirmed: bool,
},
Components,
Health,
Logs {
limit: usize,
},
@ -388,6 +400,7 @@ impl CliInvocation {
Some(CliCommand::Status) => Command::Status,
Some(CliCommand::Tasks) => Command::Tasks,
Some(CliCommand::Components) => Command::Components,
Some(CliCommand::Health) => Command::Health,
Some(CliCommand::Completions { shell }) => Command::Completions { shell },
Some(CliCommand::Man) => Command::ManPage,
Some(CliCommand::Users(users)) => match users.action {
@ -463,6 +476,9 @@ impl CliInvocation {
DaemonAction::RestartService => Command::DaemonRestartService,
DaemonAction::StopService => Command::DaemonStopService,
DaemonAction::Install { bundle, operator } => Command::Install { bundle, operator },
DaemonAction::Bootstrap { bundle, operator } => {
Command::Bootstrap { bundle, operator }
}
},
};
Ok(Self {
@ -541,6 +557,12 @@ mod tests {
assert_eq!(invocation.command, Command::UsersList);
}
#[test]
fn parses_health() {
let invocation = CliInvocation::parse(["health".into()]).unwrap();
assert_eq!(invocation.command, Command::Health);
}
#[test]
fn parses_terminal_capability_overrides() {
let invocation =
@ -599,6 +621,26 @@ mod tests {
);
}
#[test]
fn parses_bootstrap_operator() {
let invocation = CliInvocation::parse([
"daemon".into(),
"bootstrap".into(),
"--bundle".into(),
"release.zip".into(),
"--operator".into(),
"alice".into(),
])
.unwrap();
assert_eq!(
invocation.command,
Command::Bootstrap {
bundle: "release.zip".into(),
operator: Some("alice".into()),
}
);
}
#[test]
fn parses_unconfirmed_destructive_commands_explicitly() {
let invocation = CliInvocation::parse(["daemon".into(), "stop".into()]).unwrap();

View file

@ -40,33 +40,6 @@ async fn run() -> Result<(), StartupError> {
unicode,
command,
} = invocation;
let local_endpoint = match iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.map_err(|error| StartupError::Other(format!("Cannot resolve user IPC path: {error}")))?
.ipc_endpoint
{
iota_paths::IpcEndpoint::UnixSocket(path) => path,
iota_paths::IpcEndpoint::WindowsPipe(name) => {
return Err(StartupError::Other(format!(
"Windows IPC endpoint {name} is not supported by this client build"
)));
}
};
let system_endpoint = match iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
.map_err(|error| StartupError::Other(format!("Cannot resolve system IPC path: {error}")))?
.ipc_endpoint
{
iota_paths::IpcEndpoint::UnixSocket(path) => path,
iota_paths::IpcEndpoint::WindowsPipe(name) => {
return Err(StartupError::Other(format!(
"Windows IPC endpoint {name} is not supported by this client build"
)));
}
};
let endpoints = daemon_setup_flow::DaemonEndpoints {
local: local_endpoint,
system: system_endpoint,
};
match command {
Command::Help => {
print_help();
@ -91,7 +64,12 @@ async fn run() -> Result<(), StartupError> {
)
.map_err(|error| StartupError::Other(format!("Installation failed: {error}")))
}
Command::Bootstrap { bundle, operator } => {
iota_installer::bootstrap_linux_bundle(Path::new(&bundle), operator.as_deref())
.map_err(|error| StartupError::Other(format!("Bootstrap failed: {error}")))
}
command => {
let endpoints = resolve_endpoints()?;
if matches!(
command,
Command::DaemonEnable { .. }
@ -123,6 +101,32 @@ async fn run() -> Result<(), StartupError> {
}
}
fn resolve_endpoints() -> Result<daemon_setup_flow::DaemonEndpoints, StartupError> {
let local = match iota_paths::IotaPaths::resolve(iota_paths::Scope::User)
.map_err(|error| StartupError::Other(format!("Cannot resolve user IPC path: {error}")))?
.ipc_endpoint
{
iota_paths::IpcEndpoint::UnixSocket(path) => path,
iota_paths::IpcEndpoint::WindowsPipe(name) => {
return Err(StartupError::Other(format!(
"Windows IPC endpoint {name} is not supported by this client build"
)));
}
};
let system = match iota_paths::IotaPaths::resolve(iota_paths::Scope::System)
.map_err(|error| StartupError::Other(format!("Cannot resolve system IPC path: {error}")))?
.ipc_endpoint
{
iota_paths::IpcEndpoint::UnixSocket(path) => path,
iota_paths::IpcEndpoint::WindowsPipe(name) => {
return Err(StartupError::Other(format!(
"Windows IPC endpoint {name} is not supported by this client build"
)));
}
};
Ok(daemon_setup_flow::DaemonEndpoints { local, system })
}
async fn run_startup_command(command: Command) -> Result<(), StartupError> {
let manager = iota_process_manager::detect()
.await
@ -406,6 +410,7 @@ fn print_help() {
println!(" config get Show current configuration");
println!(" config set <KEY> <VAL> Set a configuration value");
println!(" config reload Reload configuration");
println!(" health Show component health");
println!(" components Show component health");
println!(" logs [--limit N] Show recent log entries");
println!(" update check Check for updates");
@ -422,6 +427,7 @@ fn print_help() {
println!(" daemon restart-service Restart the daemon service");
println!(" daemon stop-service Stop the daemon service");
println!(" daemon install Install from a bundle");
println!(" daemon bootstrap Install and enable a Linux systemd bundle");
println!(" help Show this help message");
println!(" completions <SHELL> Generate shell completions");
println!(" man Show the man page");
@ -630,6 +636,7 @@ async fn run_command(
Command::ConfigGet => LocalRequest::GetConfig,
Command::ConfigSet { key, value } => LocalRequest::SetConfig { key, value },
Command::ConfigReload => LocalRequest::ReloadConfig,
Command::Health => LocalRequest::ListComponents,
Command::Components => LocalRequest::ListComponents,
Command::Logs { limit } => LocalRequest::GetLogs { limit },
Command::UpdateCheck => LocalRequest::CheckUpdate,
@ -657,6 +664,7 @@ async fn run_command(
| Command::Completions { .. }
| Command::ManPage
| Command::Install { .. }
| Command::Bootstrap { .. }
| Command::TermsStatus { .. }
| Command::TermsShow { .. }
| Command::TermsAccept { .. }

View file

@ -24,8 +24,4 @@ tokio = { version = "1.50.0", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }
uuid = { version = "*", features = ["v4"] }
base64 = "0.22.1"
hex = "*"
rand = "0.8"
rand_core = { version = "0.6", features = ["getrandom", "std"] }
sha2 = "0.11.0"
x448 = { version = "*" }

View file

@ -39,11 +39,18 @@ use iota_util::route_target::RouteTarget;
const IOTA_KEYRING_PATH: &str = "iota.mk";
static IDENTITY_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
static OMIKRON_PUBLIC_KEY_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
/// Must be called by the daemon before any Omikron connection is attempted.
/// It keeps identity material independent from the working directory.
/*
* Keeps identity and pinned Omikron key files independent from the process
* working directory, so restarts use the same trusted material.
*/
pub fn configure_identity_path(path: PathBuf) {
let key_path = path.parent().map(|parent| parent.join("omikron.mpkb"));
let _ = IDENTITY_PATH.set(path);
if let Some(key_path) = key_path {
let _ = OMIKRON_PUBLIC_KEY_PATH.set(key_path);
}
}
fn identity_path() -> &'static Path {
IDENTITY_PATH
@ -51,7 +58,51 @@ fn identity_path() -> &'static Path {
.map(PathBuf::as_path)
.unwrap_or_else(|| Path::new(IOTA_KEYRING_PATH))
}
const OMIKRON_PUBLIC_KEY_PATH: &str = "omikron.mpkb";
fn omikron_public_key_path() -> &'static Path {
OMIKRON_PUBLIC_KEY_PATH
.get()
.map(PathBuf::as_path)
.unwrap_or_else(|| Path::new("omikron.mpkb"))
}
fn save_keyring(keyring: &Keyring, path: &Path) -> Result<(), String> {
let temporary = serialization_path(path)?;
mtp::files::save_keyring_raw(keyring, &temporary)
.map_err(|error| format!("serialize keyring: {error}"))?;
let bytes =
std::fs::read(&temporary).map_err(|error| format!("read serialized keyring: {error}"));
let _ = std::fs::remove_file(&temporary);
let bytes = bytes?;
iota_util::atomic_file::replace_private(path, &bytes, 3)
.map_err(|error| format!("write {}: {error}", path.display()))
}
fn save_omikron_public_key(key: &PublicKeyBundle, path: &Path) -> Result<(), String> {
let temporary = serialization_path(path)?;
mtp::files::save_public_key_bundle(key, &temporary)
.map_err(|error| format!("serialize Omikron public key: {error}"))?;
let bytes = std::fs::read(&temporary)
.map_err(|error| format!("read serialized Omikron public key: {error}"));
let _ = std::fs::remove_file(&temporary);
let bytes = bytes?;
iota_util::atomic_file::replace(path, &bytes, 3)
.map_err(|error| format!("write {}: {error}", path.display()))
}
fn serialization_path(path: &Path) -> Result<PathBuf, String> {
let parent = path
.parent()
.ok_or_else(|| format!("{} has no parent directory", path.display()))?;
let name = path
.file_name()
.ok_or_else(|| format!("{} has no file name", path.display()))?;
Ok(parent.join(format!(
".{}.serialize-{}",
name.to_string_lossy(),
Uuid::new_v4()
)))
}
const RECONNECT_DELAY: Duration = Duration::from_secs(5);
const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(300);
const CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
@ -434,7 +485,7 @@ impl OmikronConnection {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = mtp::files::save_keyring_raw(&keyring, path) {
if let Err(e) = save_keyring(&keyring, path) {
log!("Failed to persist {}: {}", path.display(), e);
}
@ -469,21 +520,29 @@ impl OmikronConnection {
let port: u16 = port_str
.parse()
.map_err(|_| format!("Invalid OMIKRON_PORT: {}", port_str))?;
let public_key = mtp::files::load_public_key_bundle(OMIKRON_PUBLIC_KEY_PATH)
let key_path = omikron_public_key_path();
let public_key = mtp::files::load_public_key_bundle(key_path)
.map_err(|e| {
format!(
"Failed to load Omikron public key bundle from {}: {}. Obtain {} from the Omikron operator and place it in the working directory.",
OMIKRON_PUBLIC_KEY_PATH, e, OMIKRON_PUBLIC_KEY_PATH
"Failed to load Omikron public key bundle from {}: {}. Obtain {} from the Omikron operator and place it at that path.",
key_path.display(), e, key_path.display()
)
})?;
return Ok((host, port, public_key));
}
let cached_key = mtp::files::load_public_key_bundle(OMIKRON_PUBLIC_KEY_PATH).ok();
let key_path = omikron_public_key_path();
let cached_key = mtp::files::load_public_key_bundle(key_path).ok();
let cached_host_port = {
let conf = CONFIG.load();
match (&conf.omikron_host, conf.omikron_port) {
(Some(host), Some(port)) => Some((host.clone(), port)),
(Some(host), Some(port)) if !host.trim().is_empty() && port != 0 => {
Some((host.clone(), port))
}
(Some(_), Some(_)) => {
log!("Ignoring invalid cached Omikron endpoint in Iota configuration");
None
}
_ => None,
}
};
@ -504,21 +563,35 @@ impl OmikronConnection {
let (host, port, public_key) = if let Some(endpoint) = discovered {
match &cached_key {
Some(cached) if cached.as_bytes() != endpoint.public_key.as_bytes() => {
Some(cached) => {
let keys_match =
match (cached.try_as_bytes(), endpoint.public_key.try_as_bytes()) {
(Ok(cached_bytes), Ok(discovered_bytes)) => {
cached_bytes == discovered_bytes
}
_ => false,
};
if !keys_match {
log!(
"Fetched Omikron public key differs from the cached {} - keeping the \
cached key. Delete {} manually if this is an expected key rotation.",
OMIKRON_PUBLIC_KEY_PATH,
OMIKRON_PUBLIC_KEY_PATH
key_path.display(),
key_path.display()
);
if let Some((cached_host, cached_port)) = &cached_host_port {
(cached_host.clone(), *cached_port, cached.clone())
} else {
return Err(format!(
"Omega returned an Omikron key that differs from {} and no validated cached endpoint is available",
key_path.display()
));
}
} else {
(endpoint.host, endpoint.port, cached.clone())
}
Some(cached) => (endpoint.host, endpoint.port, cached.clone()),
}
None => {
if let Err(e) = mtp::files::save_public_key_bundle(
&endpoint.public_key,
OMIKRON_PUBLIC_KEY_PATH,
) {
if let Err(e) = save_omikron_public_key(&endpoint.public_key, key_path) {
log!("Failed to cache Omikron public key: {}", e);
}
(endpoint.host, endpoint.port, endpoint.public_key)
@ -1885,7 +1958,7 @@ impl OmikronConnection {
))
})?;
}
mtp::files::save_keyring_raw(&keyring, path).map_err(|error| {
save_keyring(&keyring, path).map_err(|error| {
OmikronError::Internal(format!(
"could not save new identity {}: {error}",
path.display()
@ -2052,3 +2125,25 @@ impl OmikronClient for OmikronConnection {
Self::is_connected(self).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn durable_keyring_save_preserves_mtp_format() {
let directory = std::env::temp_dir().join(format!("iota-keyring-test-{}", Uuid::new_v4()));
std::fs::create_dir_all(&directory).unwrap();
let path = directory.join(IOTA_KEYRING_PATH);
let keyring = crypto_helper::generate_keyring();
save_keyring(&keyring, &path).unwrap();
let loaded = mtp::files::load_keyring_raw(&path).unwrap();
assert_eq!(
keyring.try_to_bytes().unwrap(),
loaded.try_to_bytes().unwrap()
);
std::fs::remove_dir_all(directory).unwrap();
}
}

View file

@ -4,57 +4,3 @@ version = "0.1.0"
edition = "2024"
[dependencies]
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
iota-logger = { path = "../iota-logger" }
iota-util = { path = "../iota-util" }
iota-storage = { path = "../iota-storage" }
iota-state = { path = "../iota-state" }
iota-auth = { path = "../iota-auth" }
actix-web = { version = "4", features = ["rustls-0_23"] }
actix-web-actors = "4"
aes-gcm = "0.10.3"
async-trait = "0.1.89"
base64 = "0.22.1"
chrono = "0.4.43"
crossterm = "*"
dashmap = "6.1.0"
futures = "*"
futures-util = "*"
hex = "*"
hkdf = "0.12.4"
hyper = { version = "1.8.1", features = [
"capi",
"client",
"full",
"http1",
"http2",
"nightly",
"server",
] }
hyper-util = { version = "*" }
json = "*"
lazy_static = "1.5.0"
once_cell = "1.21.3"
open = "5.3.3"
pnet = "0.35.0"
rand = "0.8"
rand_core = { version = "0.6", features = ["getrandom", "std"] }
ratatui = "0.30.0"
reqwest = "0.13.2"
rusqlite = "0.40.0"
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
rustls-pemfile = "2.2.0"
serde_json = "1.0.149"
sha2 = "0.11.0"
strum = "0.28.0"
strum_macros = "0.28.0"
sysinfo = "0.39.0"
tokio = { version = "1.50.0", features = ["full"] }
tokio-tungstenite = { version = "*", features = ["native-tls"] }
tungstenite = "*"
uuid = { version = "*", features = ["v4"] }
walkdir = "2.5.0"
warp = "*"
x448 = { version = "*" }
zip = "6.0.0"

View file

@ -9,6 +9,5 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = ["web-s
bytes = "1"
http = "1"
iota-logger = { path = "../iota-logger" }
iota-util = { path = "../iota-util" }
tokio = { version = "1.50.0", features = ["full"] }
tokio-util = { version = "0.7", features = ["rt"] }

View file

@ -4,57 +4,13 @@ version = "0.1.0"
edition = "2024"
[dependencies]
omikron-connector = { path = "../omikron-connector" }
iota-storage = { path = "../iota-storage" }
iota-state = { path = "../iota-state" }
iota-util = { path = "../iota-util" }
iota-logger = { path = "../iota-logger" }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git" }
actix-web = { version = "4", features = ["rustls-0_23"] }
actix-web-actors = "4"
aes-gcm = "0.10.3"
async-trait = "0.1.89"
base64 = "0.22.1"
chrono = "0.4.43"
crossterm = "*"
dashmap = "6.1.0"
futures = "*"
futures-util = "*"
hex = "*"
hkdf = "0.12.4"
hyper = { version = "1.8.1", features = [
"capi",
"client",
"full",
"http1",
"http2",
"nightly",
"server",
] }
hyper-util = { version = "*" }
json = "*"
lazy_static = "1.5.0"
once_cell = "1.21.3"
open = "5.3.3"
pnet = "0.35.0"
rand = "0.8"
rand_core = { version = "0.6", features = ["getrandom", "std"] }
ratatui = "0.30.0"
reqwest = "0.13.2"
rusqlite = "0.40.0"
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
rustls-pemfile = "2.2.0"
serde_json = "1.0.149"
sha2 = "0.11.0"
strum = "0.28.0"
strum_macros = "0.28.0"
sysinfo = "0.39.0"
tokio = { version = "1.50.0", features = ["full"] }
tokio-tungstenite = { version = "*", features = ["native-tls"] }
tungstenite = "*"
uuid = { version = "*", features = ["v4"] }
walkdir = "2.5.0"
warp = "*"
x448 = { version = "*" }
zip = "6.0.0"

View file

@ -128,7 +128,10 @@ async fn users_remove(
return forbidden();
}
let uuid = payload.get("uuid").and_then(|v| v.as_i64()).unwrap_or(0);
let uuid = match user_id(&payload) {
Ok(uuid) => uuid,
Err(response) => return response,
};
iota_storage::users::user_manager::remove_user(uuid);
iota_storage::users::user_manager::save_users();
@ -194,7 +197,14 @@ fn success() -> HttpResponse {
}
fn error() -> HttpResponse {
HttpResponse::Ok().json(json!({ "type": "error" }))
HttpResponse::BadRequest().json(json!({ "type": "error" }))
}
fn user_id(payload: &Value) -> Result<i64, HttpResponse> {
payload
.get("uuid")
.and_then(Value::as_i64)
.ok_or_else(error)
}
fn is_allowed(addr: SocketAddr, ssl: bool) -> bool {
@ -208,3 +218,20 @@ fn is_allowed_req(req: &HttpRequest, ssl: bool) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_response_is_bad_request() {
assert_eq!(error().status(), actix_web::http::StatusCode::BAD_REQUEST);
}
#[test]
fn user_id_rejects_missing_or_non_integer_uuid() {
assert!(user_id(&json!({})).is_err());
assert!(user_id(&json!({ "uuid": "0" })).is_err());
assert_eq!(user_id(&json!({ "uuid": 0 })).ok(), Some(0));
}
}