[Fix] Stability
This commit is contained in:
parent
6b4c144b73
commit
14df716cf1
19 changed files with 483 additions and 405 deletions
|
|
@ -1,2 +1,2 @@
|
||||||
[env]
|
[env]
|
||||||
MTP_TYPE_MAPS = { value = "type-maps.yaml", relative = true }
|
MTP_TYPE_MAPS = { value = "mtp-type-maps/type-maps.yaml", relative = true }
|
||||||
|
|
|
||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "mtp-type-maps"]
|
||||||
|
path = mtp-type-maps
|
||||||
|
url = ssh://git@git.methanium.net/tensamin/mtp-type-maps
|
||||||
38
Cargo.lock
generated
38
Cargo.lock
generated
|
|
@ -198,7 +198,7 @@ dependencies = [
|
||||||
"foldhash",
|
"foldhash",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"impl-more 0.3.2",
|
"impl-more 0.3.5",
|
||||||
"itoa",
|
"itoa",
|
||||||
"language-tags",
|
"language-tags",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -651,9 +651,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.3.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8"
|
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
|
|
@ -1327,9 +1327,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.16.0"
|
version = "1.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
|
|
@ -2136,9 +2136,9 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "impl-more"
|
name = "impl-more"
|
||||||
version = "0.3.2"
|
version = "0.3.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "134d2c4324d61664107020b79019cf6a6aec153f0b79bc9619ee9e794a5fb021"
|
checksum = "277ff51754a3f68f12f58446c5d006aa8baa4914ea273cce24a599cfaff33d4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
|
|
@ -2956,7 +2956,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp"
|
name = "mtp"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mtp-client",
|
"mtp-client",
|
||||||
"mtp-codec",
|
"mtp-codec",
|
||||||
|
|
@ -2972,7 +2972,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp-client"
|
name = "mtp-client"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mtp-codec",
|
"mtp-codec",
|
||||||
"mtp-common",
|
"mtp-common",
|
||||||
|
|
@ -2985,7 +2985,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp-codec"
|
name = "mtp-codec"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
|
@ -2998,7 +2998,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp-common"
|
name = "mtp-common"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quinn",
|
"quinn",
|
||||||
"rustls",
|
"rustls",
|
||||||
|
|
@ -3009,7 +3009,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp-crypto"
|
name = "mtp-crypto"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
|
|
@ -3031,7 +3031,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp-files"
|
name = "mtp-files"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mtp-crypto",
|
"mtp-crypto",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
|
|
@ -3042,7 +3042,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp-host"
|
name = "mtp-host"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mtp-codec",
|
"mtp-codec",
|
||||||
"mtp-common",
|
"mtp-common",
|
||||||
|
|
@ -3057,7 +3057,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp-transport"
|
name = "mtp-transport"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"mtp-codec",
|
"mtp-codec",
|
||||||
|
|
@ -3075,7 +3075,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp-type-map"
|
name = "mtp-type-map"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
|
@ -3084,7 +3084,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mtp-webserver"
|
name = "mtp-webserver"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://git.methanium.net/Methanium/mtp.git#00f0aaeeff802c716f143fefe0117470cc5a1738"
|
source = "git+https://git.methanium.net/Methanium/mtp.git#88ae866b91857c9b1650ea0ee2af2fdb70b92f10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|
@ -4263,9 +4263,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.15.0"
|
version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"web-time",
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
|
|
|
||||||
|
|
@ -594,7 +594,7 @@ impl IpcClient {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
match tokio::time::timeout(Duration::from_secs(30), response_rx).await {
|
match tokio::time::timeout(Duration::from_secs(45), response_rx).await {
|
||||||
Ok(Ok(result)) => Ok(result),
|
Ok(Ok(result)) => Ok(result),
|
||||||
Ok(Err(_)) => Ok(ResponseResult::Error(iota_ipc::IpcErrorCode::Disconnected)),
|
Ok(Err(_)) => Ok(ResponseResult::Error(iota_ipc::IpcErrorCode::Disconnected)),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,17 @@ impl OverviewScreen {
|
||||||
|
|
||||||
lines.push(Line::from(Span::styled("Connection", theme.text.heading)));
|
lines.push(Line::from(Span::styled("Connection", theme.text.heading)));
|
||||||
lines.push(Line::from(format!(" State: {}", connection_label(&conn))));
|
lines.push(Line::from(format!(" State: {}", connection_label(&conn))));
|
||||||
|
let omikron = daemon.components.get(&iota_ipc::ComponentId::Omikron);
|
||||||
|
let omikron_label = match omikron.map(|health| health.status) {
|
||||||
|
Some(iota_ipc::HealthStatus::Healthy) => "[OK] Connected",
|
||||||
|
Some(iota_ipc::HealthStatus::Degraded) => "[WARN] Connecting or unavailable",
|
||||||
|
Some(iota_ipc::HealthStatus::Failed) => "[FAIL] Authentication failed",
|
||||||
|
None => "Unknown",
|
||||||
|
};
|
||||||
|
lines.push(Line::from(format!(" Omikron: {omikron_label}")));
|
||||||
|
if let Some(message) = omikron.and_then(|health| health.message.as_deref()) {
|
||||||
|
lines.push(Line::from(format!(" Omikron detail: {message}")));
|
||||||
|
}
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
lines.push(Line::from(Span::styled("Daemon", theme.text.heading)));
|
lines.push(Line::from(Span::styled("Daemon", theme.text.heading)));
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use iota_ipc::{
|
||||||
};
|
};
|
||||||
use iota_logger::{log, log_command};
|
use iota_logger::{log, log_command};
|
||||||
use iota_storage::users::user_manager;
|
use iota_storage::users::user_manager;
|
||||||
use iota_storage::util::config_util::{self, modify_config};
|
use iota_storage::util::config_util::{self};
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue};
|
use mtp::codec::{CommunicationType, CommunicationValue};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
@ -52,10 +52,7 @@ impl CommandRouter {
|
||||||
}
|
}
|
||||||
let needs_omikron = matches!(
|
let needs_omikron = matches!(
|
||||||
request,
|
request,
|
||||||
LocalRequest::CreateUser { .. }
|
LocalRequest::CreateUser { .. } | LocalRequest::RemoveUser { .. }
|
||||||
| LocalRequest::RemoveUser { .. }
|
|
||||||
| LocalRequest::ReconnectOmikron
|
|
||||||
| LocalRequest::RotateIotaIdentity
|
|
||||||
);
|
);
|
||||||
if needs_omikron && !self.services.omikron.is_connected().await {
|
if needs_omikron && !self.services.omikron.is_connected().await {
|
||||||
return ResponseResult::Error(
|
return ResponseResult::Error(
|
||||||
|
|
@ -112,11 +109,33 @@ impl CommandRouter {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
(Some(user), _) => ResponseResult::Ok(ResponsePayload::UserCreated {
|
Ok(user) => ResponseResult::Ok(ResponsePayload::UserCreated {
|
||||||
user_id: user.user_id,
|
user_id: user.user_id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
}),
|
}),
|
||||||
_ => ResponseResult::Error(IpcErrorCode::StorageFailure),
|
Err(error) => {
|
||||||
|
log!("User creation failed: {error:?}");
|
||||||
|
match error {
|
||||||
|
omikron_connector::user_ops::CreateUserError::InvalidUsername => {
|
||||||
|
ResponseResult::Error(IpcErrorCode::InvalidRequest)
|
||||||
|
}
|
||||||
|
omikron_connector::user_ops::CreateUserError::Transport(
|
||||||
|
omikron_connector::OmikronError::Timeout(_),
|
||||||
|
) => ResponseResult::Error(IpcErrorCode::Timeout),
|
||||||
|
omikron_connector::user_ops::CreateUserError::Transport(_) => {
|
||||||
|
ResponseResult::Error(IpcErrorCode::OmikronUnavailable)
|
||||||
|
}
|
||||||
|
omikron_connector::user_ops::CreateUserError::RemoteRejected => {
|
||||||
|
ResponseResult::Error(IpcErrorCode::Conflict)
|
||||||
|
}
|
||||||
|
omikron_connector::user_ops::CreateUserError::LocalPersistence(_) => {
|
||||||
|
ResponseResult::Error(IpcErrorCode::StorageFailure)
|
||||||
|
}
|
||||||
|
omikron_connector::user_ops::CreateUserError::InvalidResponse => {
|
||||||
|
ResponseResult::Error(IpcErrorCode::InternalFailure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LocalRequest::RemoveUser { user_id } => {
|
LocalRequest::RemoveUser { user_id } => {
|
||||||
|
|
@ -139,16 +158,14 @@ impl CommandRouter {
|
||||||
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
|
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
|
||||||
},
|
},
|
||||||
LocalRequest::RotateIotaIdentity => {
|
LocalRequest::RotateIotaIdentity => {
|
||||||
modify_config(|config| {
|
match self.services.omikron.rotate_identity().await {
|
||||||
config.public_key = None;
|
|
||||||
config.private_key = None;
|
|
||||||
config.iota_id = None;
|
|
||||||
});
|
|
||||||
match self.services.omikron.reconnect().await {
|
|
||||||
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
|
Ok(()) => ResponseResult::Ok(ResponsePayload::Acknowledged {
|
||||||
message: "Key pair regenerated and Omikron reconnection requested".into(),
|
message: "New identity registered with Omikron".into(),
|
||||||
}),
|
}),
|
||||||
Err(_) => ResponseResult::Error(IpcErrorCode::OmikronUnavailable),
|
Err(error) => {
|
||||||
|
log!("Iota identity rotation failed: {}", error);
|
||||||
|
ResponseResult::Error(IpcErrorCode::OmikronUnavailable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LocalRequest::RequestProcessExit { intent } => {
|
LocalRequest::RequestProcessExit { intent } => {
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,11 @@ impl OmikronClient for InactiveOmikron {
|
||||||
"terms have not been accepted".into(),
|
"terms have not been accepted".into(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
async fn rotate_identity(&self) -> Result<(), OmikronError> {
|
||||||
|
Err(OmikronError::Disconnected(
|
||||||
|
"terms have not been accepted".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
async fn is_connected(&self) -> bool {
|
async fn is_connected(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,12 @@ impl OmikronClient for FakeOmikron {
|
||||||
self.reconnects.fetch_add(1, Ordering::SeqCst);
|
self.reconnects.fetch_add(1, Ordering::SeqCst);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
async fn rotate_identity(&self) -> Result<(), OmikronError> {
|
||||||
|
self.reconnects.fetch_add(1, Ordering::SeqCst);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
async fn is_connected(&self) -> bool {
|
async fn is_connected(&self) -> bool {
|
||||||
true
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,6 +47,7 @@ async fn reconnect_uses_the_injected_client() {
|
||||||
omikron: fake.clone(),
|
omikron: fake.clone(),
|
||||||
users: Default::default(),
|
users: Default::default(),
|
||||||
config: Default::default(),
|
config: Default::default(),
|
||||||
|
active: true,
|
||||||
});
|
});
|
||||||
let router = CommandRouter::new(
|
let router = CommandRouter::new(
|
||||||
Arc::new(DaemonRuntime::new()),
|
Arc::new(DaemonRuntime::new()),
|
||||||
|
|
@ -55,3 +60,29 @@ async fn reconnect_uses_the_injected_client() {
|
||||||
));
|
));
|
||||||
assert_eq!(fake.reconnects.load(Ordering::SeqCst), 1);
|
assert_eq!(fake.reconnects.load(Ordering::SeqCst), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn identity_rotation_is_available_while_omikron_is_offline() {
|
||||||
|
let fake = Arc::new(FakeOmikron {
|
||||||
|
reconnects: AtomicUsize::new(0),
|
||||||
|
});
|
||||||
|
let services = Arc::new(DaemonServices {
|
||||||
|
omikron: fake.clone(),
|
||||||
|
users: Default::default(),
|
||||||
|
config: Default::default(),
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
let router = CommandRouter::new(
|
||||||
|
Arc::new(DaemonRuntime::new()),
|
||||||
|
services,
|
||||||
|
Arc::new(Mutex::new(LogBuffer::new(100))),
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
router
|
||||||
|
.route(1, LocalRequest::RotateIotaIdentity)
|
||||||
|
.await
|
||||||
|
.result,
|
||||||
|
ResponseResult::Ok(_)
|
||||||
|
));
|
||||||
|
assert_eq!(fake.reconnects.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,19 +140,75 @@ async fn main() -> ExitCode {
|
||||||
);
|
);
|
||||||
connection
|
connection
|
||||||
}
|
}
|
||||||
Err(omikron_connector::OmikronStartupError::Authentication) => {
|
Err(omikron_connector::OmikronStartupError::Authentication { connection }) => {
|
||||||
runtime.set_component_failed(
|
runtime.set_component_failed(
|
||||||
iota_ipc::ComponentId::Omikron,
|
iota_ipc::ComponentId::Omikron,
|
||||||
"Omikron authentication failed".into(),
|
"Omikron authentication failed; regenerate the Iota identity to register again"
|
||||||
|
.into(),
|
||||||
);
|
);
|
||||||
return ExitCode::FAILURE;
|
// Keep IPC alive: identity rotation is the supported recovery
|
||||||
|
// action and must remain available after authentication fails.
|
||||||
|
connection
|
||||||
}
|
}
|
||||||
Err(omikron_connector::OmikronStartupError::Construction(error)) => {
|
Err(omikron_connector::OmikronStartupError::Construction(error)) => {
|
||||||
eprintln!("Cannot construct Omikron connection: {error}");
|
eprintln!("Cannot construct Omikron connection: {error}");
|
||||||
return ExitCode::FAILURE;
|
return ExitCode::FAILURE;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let omikron_health = omikron.clone();
|
||||||
let services = DaemonServices::new(omikron);
|
let services = DaemonServices::new(omikron);
|
||||||
|
let health_runtime = runtime.clone();
|
||||||
|
runtime
|
||||||
|
.tasks
|
||||||
|
.spawn_tracked("omikron-health", async move {
|
||||||
|
let mut states = omikron_health.connection_state();
|
||||||
|
loop {
|
||||||
|
let state = *states.borrow();
|
||||||
|
match state {
|
||||||
|
omikron_connector::omikron_connection::ConnectionState::Connected {
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let ping_ms = *omikron_health.last_ping.lock().await;
|
||||||
|
let message = if ping_ms >= 0 {
|
||||||
|
format!("connected (RTT: {ping_ms} ms)")
|
||||||
|
} else {
|
||||||
|
"connected (waiting for RTT sample)".into()
|
||||||
|
};
|
||||||
|
health_runtime
|
||||||
|
.set_component_healthy(iota_ipc::ComponentId::Omikron, Some(message));
|
||||||
|
}
|
||||||
|
omikron_connector::omikron_connection::ConnectionState::Connecting => {
|
||||||
|
health_runtime.set_component_degraded(
|
||||||
|
iota_ipc::ComponentId::Omikron,
|
||||||
|
"connecting to Omikron".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
omikron_connector::omikron_connection::ConnectionState::Disconnected => {
|
||||||
|
let message = omikron_health
|
||||||
|
.get_auth_failure()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|| "disconnected; retrying".into());
|
||||||
|
if omikron_health.has_auth_failure().await {
|
||||||
|
health_runtime
|
||||||
|
.set_component_failed(iota_ipc::ComponentId::Omikron, message);
|
||||||
|
} else {
|
||||||
|
health_runtime
|
||||||
|
.set_component_degraded(iota_ipc::ComponentId::Omikron, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::select! {
|
||||||
|
changed = states.changed() => if changed.is_err() { break },
|
||||||
|
// RTT is updated by MTP's heartbeat independently of a
|
||||||
|
// connection-state transition, so periodically refresh
|
||||||
|
// the component detail while connected.
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(1)) => {},
|
||||||
|
_ = health_runtime.cancellation.cancelled() => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await;
|
||||||
let ipc_server = match IpcServer::bind(
|
let ipc_server = match IpcServer::bind(
|
||||||
socket.clone(),
|
socket.clone(),
|
||||||
runtime.clone(),
|
runtime.clone(),
|
||||||
|
|
|
||||||
|
|
@ -171,5 +171,3 @@ fn run(program: &str, args: &[&str]) -> Result<()> {
|
||||||
bail!("{program} failed; run the installer as root")
|
bail!("{program} failed; run the installer as root")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,19 +98,19 @@ pub fn startup_with_log_dir(log_dir: Option<std::path::PathBuf>) {
|
||||||
format!("{} ", msg.prefix)
|
format!("{} ", msg.prefix)
|
||||||
};
|
};
|
||||||
|
|
||||||
let line1 = format!(
|
let line = format!(
|
||||||
"{} {}{}",
|
"{} {} {}{}",
|
||||||
fixed_box(&msg.timestamp_ms.to_string(), 13),
|
fixed_box(&msg.timestamp_ms.to_string(), 13),
|
||||||
|
timestamp,
|
||||||
prefix,
|
prefix,
|
||||||
resolved_message
|
resolved_message
|
||||||
);
|
);
|
||||||
let line2 = format!(" {}", timestamp);
|
|
||||||
|
|
||||||
if let Some(file) = file.as_mut() {
|
if let Some(file) = file.as_mut() {
|
||||||
let _ = writeln!(file, "{}\n{}", line1, line2);
|
let _ = writeln!(file, "{}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = writeln!(std::io::stderr(), "{}\n{}", line1, line2);
|
let _ = writeln!(std::io::stderr(), "{}", line);
|
||||||
|
|
||||||
let entry = UiLogEntry {
|
let entry = UiLogEntry {
|
||||||
timestamp_ms: msg.timestamp_ms,
|
timestamp_ms: msg.timestamp_ms,
|
||||||
|
|
@ -329,8 +329,11 @@ pub fn format_cv(cv: &CommunicationValue) -> String {
|
||||||
parts.push(format!("> {}", receiver));
|
parts.push(format!("> {}", receiver));
|
||||||
}
|
}
|
||||||
|
|
||||||
let comm_type = cv.get_type().to_string();
|
let comm_type = cv
|
||||||
parts.push(format!("{}", comm_type));
|
.get_comm_type_enum()
|
||||||
|
.map(|kind| kind.to_string())
|
||||||
|
.unwrap_or_else(|| cv.get_type().to_string());
|
||||||
|
parts.push(format!("{} (id={})", comm_type, cv.get_id()));
|
||||||
|
|
||||||
let data = cv.data();
|
let data = cv.data();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,13 @@ use rand_core::{OsRng, RngCore};
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
|
|
||||||
pub fn add_user(user: UserProfile) {
|
pub fn add_user(user: UserProfile) {
|
||||||
if let Err(e) = db::with_db(|conn| {
|
if let Err(e) = try_add_user(user) {
|
||||||
|
eprintln!("Failed to add_user: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_add_user(user: UserProfile) -> Result<(), crate::storage_error::StorageError> {
|
||||||
|
db::with_db(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO users (user_id, username, public_key, private_key_hash, reset_token, created_at, display_name)
|
INSERT INTO users (user_id, username, public_key, private_key_hash, reset_token, created_at, display_name)
|
||||||
|
|
@ -40,9 +46,7 @@ pub fn add_user(user: UserProfile) {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}) {
|
})
|
||||||
eprintln!("Failed to add_user: {}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_user(user: UserProfile) {
|
pub fn update_user(user: UserProfile) {
|
||||||
|
|
|
||||||
|
|
@ -239,5 +239,3 @@ fn pending_forward_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Pending
|
||||||
created_at: row.get(8)?,
|
created_at: row.get(8)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,41 +105,28 @@ pub fn load_file_vec(path: &str, name: &str) -> Result<Vec<u8>, std::io::Error>
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_file(path: &str, name: &str, value: &str) {
|
pub fn save_file(path: &str, name: &str, value: &str) {
|
||||||
let Ok(file_path) = storage_file(path, name) else {
|
if let Err(error) = try_save_file(path, name, value) {
|
||||||
eprintln!("[IMPORTANT] Refusing unsafe storage path");
|
eprintln!("[IMPORTANT] Couldn't save file: {error}");
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(dir) = file_path.parent() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !dir.exists() {
|
|
||||||
if let Err(e) = fs::create_dir_all(&dir) {
|
|
||||||
println!("[IMPORTANT] Couldn't create directories: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_save_file(path: &str, name: &str, value: &str) -> io::Result<()> {
|
||||||
|
let file_path = storage_file(path, name)?;
|
||||||
|
let dir = file_path
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "file has no parent"))?;
|
||||||
|
|
||||||
|
fs::create_dir_all(dir)?;
|
||||||
|
|
||||||
// Write to a temp file first, then atomically rename to prevent partial writes.
|
// Write to a temp file first, then atomically rename to prevent partial writes.
|
||||||
let tmp_name = format!(".{}.tmp", name);
|
let tmp_name = format!(".{}.tmp", name);
|
||||||
let tmp_path = dir.join(&tmp_name);
|
let tmp_path = dir.join(&tmp_name);
|
||||||
if let Err(e) = fs::write(&tmp_path, value) {
|
fs::write(&tmp_path, value)?;
|
||||||
println!(
|
if let Err(error) = fs::rename(&tmp_path, &file_path) {
|
||||||
"[IMPORTANT] Couldn't write temp file {}: {}",
|
|
||||||
tmp_path.display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Err(e) = fs::rename(&tmp_path, &file_path) {
|
|
||||||
println!(
|
|
||||||
"[IMPORTANT] Couldn't rename {} to {}: {}",
|
|
||||||
tmp_path.display(),
|
|
||||||
file_path.display(),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
let _ = fs::remove_file(&tmp_path);
|
let _ = fs::remove_file(&tmp_path);
|
||||||
|
return Err(error);
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_children(path: &str) -> Vec<String> {
|
pub fn get_children(path: &str) -> Vec<String> {
|
||||||
|
|
|
||||||
1
mtp-type-maps
Submodule
1
mtp-type-maps
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 594646ac39d986f0787aa614a99d580035a67318
|
||||||
|
|
@ -27,7 +27,9 @@ pub enum OmikronStartupError {
|
||||||
InitialConnectionTimeout {
|
InitialConnectionTimeout {
|
||||||
connection: std::sync::Arc<crate::omikron_connection::OmikronConnection>,
|
connection: std::sync::Arc<crate::omikron_connection::OmikronConnection>,
|
||||||
},
|
},
|
||||||
Authentication,
|
Authentication {
|
||||||
|
connection: std::sync::Arc<crate::omikron_connection::OmikronConnection>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -39,5 +41,9 @@ pub trait OmikronClient: Send + Sync {
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
) -> Result<CommunicationValue, OmikronError>;
|
) -> Result<CommunicationValue, OmikronError>;
|
||||||
async fn reconnect(&self) -> Result<(), OmikronError>;
|
async fn reconnect(&self) -> Result<(), OmikronError>;
|
||||||
|
/// Replace the local Iota identity and wait for the new identity to
|
||||||
|
/// register/authenticate. This is deliberately available while offline:
|
||||||
|
/// it is the recovery operation for an authentication failure.
|
||||||
|
async fn rotate_identity(&self) -> Result<(), OmikronError>;
|
||||||
async fn is_connected(&self) -> bool;
|
async fn is_connected(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::{Arc, LazyLock};
|
use std::sync::{Arc, LazyLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
use tokio::sync::{Mutex, RwLock, Semaphore, oneshot, watch};
|
use tokio::sync::{Mutex, RwLock, Semaphore, oneshot, watch};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
@ -221,6 +221,11 @@ impl OmikronConnection {
|
||||||
let _ = self.state_watch_tx.send(new_state);
|
let _ = self.state_watch_tx.send(new_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Subscribe to connection transitions for daemon health reporting.
|
||||||
|
pub fn connection_state(&self) -> watch::Receiver<ConnectionState> {
|
||||||
|
self.state_watch_tx.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Connection Management
|
// Connection Management
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
@ -1880,10 +1885,11 @@ impl OmikronConnection {
|
||||||
let reason = response_cv
|
let reason = response_cv
|
||||||
.get_data(DataType::Message)
|
.get_data(DataType::Message)
|
||||||
.as_str()
|
.as_str()
|
||||||
|
.or_else(|| response_cv.get_data(DataType::ErrorType).as_str())
|
||||||
.unwrap_or("connection error")
|
.unwrap_or("connection error")
|
||||||
.to_string();
|
.to_string();
|
||||||
Err(format!(
|
Err(format!(
|
||||||
"Request failed due to disconnect (msg_id={}, reason={})",
|
"Request rejected (msg_id={}, reason={})",
|
||||||
msg_id, reason
|
msg_id, reason
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1955,6 +1961,75 @@ impl OmikronConnection {
|
||||||
self.stop().await;
|
self.stop().await;
|
||||||
self.connect().await;
|
self.connect().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new local keyring and register it as a new Iota identity.
|
||||||
|
/// The existing keyring is retained as a timestamped backup so a failed
|
||||||
|
/// recovery does not silently destroy the user's previous identity.
|
||||||
|
pub async fn rotate_identity(self: &Arc<Self>) -> Result<(), OmikronError> {
|
||||||
|
log!("Iota identity rotation requested");
|
||||||
|
self.stop().await;
|
||||||
|
|
||||||
|
let path = identity_path();
|
||||||
|
if path.exists() {
|
||||||
|
let stamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis();
|
||||||
|
let backup = path.with_extension(format!("mk.backup-{stamp}"));
|
||||||
|
std::fs::rename(path, &backup).map_err(|error| {
|
||||||
|
OmikronError::Internal(format!(
|
||||||
|
"could not back up identity {}: {error}",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
log!("Existing Iota identity backed up to {}", backup.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyring = crypto_helper::generate_keyring();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|error| {
|
||||||
|
OmikronError::Internal(format!(
|
||||||
|
"could not create identity directory {}: {error}",
|
||||||
|
parent.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
mtp::files::save_keyring_raw(&keyring, path).map_err(|error| {
|
||||||
|
OmikronError::Internal(format!(
|
||||||
|
"could not save new identity {}: {error}",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
modify_config(|config| {
|
||||||
|
config.iota_id = None;
|
||||||
|
config.keyring = None;
|
||||||
|
config.public_key = None;
|
||||||
|
config.private_key = None;
|
||||||
|
});
|
||||||
|
log!("New Iota identity generated; registration started");
|
||||||
|
|
||||||
|
self.clear_auth_failure().await;
|
||||||
|
self.connect().await;
|
||||||
|
match self.await_connection(Some(CONNECTION_TIMEOUT)).await {
|
||||||
|
Ok(()) => {
|
||||||
|
let id = CONFIG.load().iota_id;
|
||||||
|
log!(
|
||||||
|
"New Iota identity registered{}",
|
||||||
|
id.map(|v| format!(" (Iota-ID: {v})")).unwrap_or_default()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(timeout) => {
|
||||||
|
if let Some(reason) = self.get_auth_failure().await {
|
||||||
|
log!("Iota identity registration failed: {}", reason);
|
||||||
|
Err(OmikronError::Authentication(reason))
|
||||||
|
} else {
|
||||||
|
log!("Iota identity registration did not complete: {}", timeout);
|
||||||
|
Err(OmikronError::Timeout(timeout))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -1975,7 +2050,7 @@ pub async fn connect_initial(
|
||||||
match conn.await_connection(Some(CONNECTION_TIMEOUT)).await {
|
match conn.await_connection(Some(CONNECTION_TIMEOUT)).await {
|
||||||
Ok(()) => Ok(conn),
|
Ok(()) => Ok(conn),
|
||||||
Err(_) if conn.has_auth_failure().await => {
|
Err(_) if conn.has_auth_failure().await => {
|
||||||
Err(crate::client::OmikronStartupError::Authentication)
|
Err(crate::client::OmikronStartupError::Authentication { connection: conn })
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
Err(crate::client::OmikronStartupError::InitialConnectionTimeout { connection: conn })
|
Err(crate::client::OmikronStartupError::InitialConnectionTimeout { connection: conn })
|
||||||
|
|
@ -2027,6 +2102,8 @@ impl OmikronClient for OmikronConnection {
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
if error.contains("timed out") {
|
if error.contains("timed out") {
|
||||||
OmikronError::Timeout(error)
|
OmikronError::Timeout(error)
|
||||||
|
} else if error.starts_with("Request rejected") {
|
||||||
|
OmikronError::Internal(error)
|
||||||
} else {
|
} else {
|
||||||
OmikronError::Disconnected(error)
|
OmikronError::Disconnected(error)
|
||||||
}
|
}
|
||||||
|
|
@ -2057,6 +2134,29 @@ impl OmikronClient for OmikronConnection {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn rotate_identity(&self) -> Result<(), OmikronError> {
|
||||||
|
let this = Arc::new(Self {
|
||||||
|
state: self.state.clone(),
|
||||||
|
state_watch_tx: self.state_watch_tx.clone(),
|
||||||
|
sender: self.sender.clone(),
|
||||||
|
connection_loop_handle: self.connection_loop_handle.clone(),
|
||||||
|
last_ping: self.last_ping.clone(),
|
||||||
|
heartbeat_handle: self.heartbeat_handle.clone(),
|
||||||
|
connection_id: self.connection_id,
|
||||||
|
shutdown_tx: self.shutdown_tx.clone(),
|
||||||
|
reconnect_on_close: self.reconnect_on_close.clone(),
|
||||||
|
auth_failure: self.auth_failure.clone(),
|
||||||
|
app_challenges: self.app_challenges.clone(),
|
||||||
|
app_sessions: self.app_sessions.clone(),
|
||||||
|
missed_pongs: self.missed_pongs.clone(),
|
||||||
|
handler_semaphore: self.handler_semaphore.clone(),
|
||||||
|
cancellation: self.cancellation.clone(),
|
||||||
|
active_tasks: self.active_tasks.clone(),
|
||||||
|
app: self.app.clone(),
|
||||||
|
});
|
||||||
|
Self::rotate_identity(&this).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn is_connected(&self) -> bool {
|
async fn is_connected(&self) -> bool {
|
||||||
Self::is_connected(self).await
|
Self::is_connected(self).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||||
use iota_logger::{PrintType, log, log_cv, log_t};
|
use iota_logger::{PrintType, log, log_cv, log_t};
|
||||||
use iota_storage::users::user_manager::{add_user, save_users};
|
use iota_storage::users::user_manager::try_add_user;
|
||||||
use iota_storage::users::user_profile::UserProfile;
|
use iota_storage::users::user_profile::UserProfile;
|
||||||
use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64};
|
use iota_util::crypto_helper::{self, hex_hash, public_key_bundle_to_base64};
|
||||||
use iota_util::file_util::save_file;
|
use iota_util::file_util::try_save_file;
|
||||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
use rand_core::{OsRng, RngCore};
|
use rand_core::{OsRng, RngCore};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
@ -11,34 +11,75 @@ use std::time::Duration;
|
||||||
use crate::OmikronClient;
|
use crate::OmikronClient;
|
||||||
use crate::omega_discovery;
|
use crate::omega_discovery;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CreateUserError {
|
||||||
|
InvalidUsername,
|
||||||
|
Transport(crate::OmikronError),
|
||||||
|
InvalidResponse,
|
||||||
|
RemoteRejected,
|
||||||
|
LocalPersistence(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_username(username: &str) -> bool {
|
||||||
|
!username.is_empty()
|
||||||
|
&& username.chars().count() <= 15
|
||||||
|
&& !username.chars().any(char::is_control)
|
||||||
|
&& !username.contains(['/', '\\'])
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn request_user_id(connection: &dyn OmikronClient) -> Result<(i64, String), CreateUserError> {
|
||||||
|
let request = CommunicationValue::new(CommunicationType::GetRegister);
|
||||||
|
let response = connection
|
||||||
|
.await_response(&request, Duration::from_secs(20))
|
||||||
|
.await
|
||||||
|
.map_err(CreateUserError::Transport)?;
|
||||||
|
|
||||||
|
if !response.is_type(CommunicationType::GetRegister) {
|
||||||
|
return Err(CreateUserError::InvalidResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_id = response
|
||||||
|
.get_data(DataType::UserId)
|
||||||
|
.as_number()
|
||||||
|
.and_then(|id| i64::try_from(id).ok())
|
||||||
|
.filter(|id| (1..(1_i64 << 48)).contains(id))
|
||||||
|
.ok_or(CreateUserError::InvalidResponse)?;
|
||||||
|
let registration_token = response
|
||||||
|
.get_data(DataType::RegisterId)
|
||||||
|
.as_str()
|
||||||
|
.filter(|token| uuid::Uuid::parse_str(token).is_ok())
|
||||||
|
.map(str::to_owned)
|
||||||
|
.ok_or(CreateUserError::InvalidResponse)?;
|
||||||
|
Ok((user_id, registration_token))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A completion response can be lost after Omega commits the user. Confirm
|
||||||
|
/// the exact remote record before treating that transport failure as success.
|
||||||
|
async fn registration_committed(connection: &dyn OmikronClient, profile: &UserProfile) -> bool {
|
||||||
|
let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default(
|
||||||
|
DataType::UserId,
|
||||||
|
DataValue::SignedNumber(profile.user_id.into()),
|
||||||
|
);
|
||||||
|
let Ok(response) = connection
|
||||||
|
.await_response(&request, Duration::from_secs(5))
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
response.get_data(DataType::UserId).as_number() == Some(profile.user_id.into())
|
||||||
|
&& response.get_data(DataType::Username).as_str() == Some(profile.username.as_str())
|
||||||
|
&& response.get_data(DataType::PublicKey).as_str() == Some(profile.public_key.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
connection: &dyn OmikronClient,
|
connection: &dyn OmikronClient,
|
||||||
username: &str,
|
username: &str,
|
||||||
) -> (Option<UserProfile>, Option<String>) {
|
) -> Result<UserProfile, CreateUserError> {
|
||||||
let register_communication_value = CommunicationValue::new(CommunicationType::GetRegister);
|
if !valid_username(username) {
|
||||||
|
return Err(CreateUserError::InvalidUsername);
|
||||||
let response_communication_value = match connection
|
}
|
||||||
.await_response(®ister_communication_value, Duration::from_secs(20))
|
let (user_id, registration_token) = request_user_id(connection).await?;
|
||||||
.await
|
log!("User creation: Omega allocated user ID {user_id}");
|
||||||
{
|
|
||||||
Ok(communication_value) => communication_value,
|
|
||||||
Err(e) => {
|
|
||||||
log_t!("User creation: {}", e.to_string());
|
|
||||||
return (None, None);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
log_cv!(PrintType::Omega, response_communication_value);
|
|
||||||
|
|
||||||
let user_id = match response_communication_value
|
|
||||||
.get_data(DataType::UserId)
|
|
||||||
.as_number()
|
|
||||||
{
|
|
||||||
Some(id) => id,
|
|
||||||
None => {
|
|
||||||
log_t!("User creation: Response returned none");
|
|
||||||
return (None, None);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let keyring = crypto_helper::generate_keyring();
|
let keyring = crypto_helper::generate_keyring();
|
||||||
let pub_key_bundle = keyring.public_key_bundle();
|
let pub_key_bundle = keyring.public_key_bundle();
|
||||||
let keyring_b64 = crypto_helper::keyring_to_base64(&keyring);
|
let keyring_b64 = crypto_helper::keyring_to_base64(&keyring);
|
||||||
|
|
@ -50,7 +91,7 @@ pub async fn create_user(
|
||||||
let reset_token = STANDARD.encode(&bytes);
|
let reset_token = STANDARD.encode(&bytes);
|
||||||
|
|
||||||
let user_profile = UserProfile::new(
|
let user_profile = UserProfile::new(
|
||||||
user_id as i64,
|
user_id,
|
||||||
username.to_string(),
|
username.to_string(),
|
||||||
None,
|
None,
|
||||||
public_key_bundle_to_base64(&pub_key_bundle),
|
public_key_bundle_to_base64(&pub_key_bundle),
|
||||||
|
|
@ -59,30 +100,43 @@ pub async fn create_user(
|
||||||
);
|
);
|
||||||
|
|
||||||
let communication_value = CommunicationValue::new(CommunicationType::CompleteRegisterUser)
|
let communication_value = CommunicationValue::new(CommunicationType::CompleteRegisterUser)
|
||||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id as i128))
|
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()))
|
||||||
.add_typed_default(DataType::Username, DataValue::Str(username.to_string()))
|
.add_typed_default(DataType::Username, DataValue::Str(username.to_string()))
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::PublicKey,
|
DataType::PublicKey,
|
||||||
DataValue::Str(public_key_bundle_to_base64(&pub_key_bundle)),
|
DataValue::Str(public_key_bundle_to_base64(&pub_key_bundle)),
|
||||||
)
|
)
|
||||||
.add_typed_default(DataType::IotaId, DataValue::SignedNumber(user_id as i128))
|
.add_typed_default(DataType::ResetToken, DataValue::Str(reset_token))
|
||||||
.add_typed_default(DataType::ResetToken, DataValue::Str(reset_token));
|
.add_typed_default(DataType::RegisterId, DataValue::Str(registration_token));
|
||||||
|
|
||||||
let response_communication_value = connection
|
let response_communication_value = connection
|
||||||
.await_response(&communication_value, Duration::from_secs(20))
|
.await_response(&communication_value, Duration::from_secs(20))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Ok(response) = response_communication_value {
|
match response_communication_value {
|
||||||
log_cv!(PrintType::Omega, response);
|
Ok(response) => {
|
||||||
if !response.is_type(CommunicationType::Success) {
|
log_cv!(PrintType::Omega, response);
|
||||||
return (None, None);
|
if !response.is_type(CommunicationType::Success) {
|
||||||
|
return Err(CreateUserError::RemoteRejected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
if registration_committed(connection, &user_profile).await {
|
||||||
|
log!(
|
||||||
|
"User creation: completion response was lost; verified user {} remotely",
|
||||||
|
user_id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log_t!("User creation: {}", error.to_string());
|
||||||
|
return Err(match error {
|
||||||
|
crate::OmikronError::Internal(_) => CreateUserError::RemoteRejected,
|
||||||
|
error => CreateUserError::Transport(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log_t!("User creation: Response returned none");
|
|
||||||
return (None, None);
|
|
||||||
}
|
}
|
||||||
log!("Created User");
|
log!("Created User");
|
||||||
save_file(
|
try_save_file(
|
||||||
"",
|
"",
|
||||||
&format!("{}.tu", username),
|
&format!("{}.tu", username),
|
||||||
&format!(
|
&format!(
|
||||||
|
|
@ -91,9 +145,91 @@ pub async fn create_user(
|
||||||
omega_discovery::omega_host(),
|
omega_discovery::omega_host(),
|
||||||
keyring_b64
|
keyring_b64
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
|
.map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?;
|
||||||
|
|
||||||
add_user(user_profile.clone());
|
try_add_user(user_profile.clone())
|
||||||
save_users();
|
.map_err(|error| CreateUserError::LocalPersistence(error.to_string()))?;
|
||||||
(Some(user_profile), Some(keyring_b64))
|
Ok(user_profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{CreateUserError, request_user_id, valid_username};
|
||||||
|
use crate::{OmikronClient, OmikronError};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
struct RegistrationClient {
|
||||||
|
response: CommunicationValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl OmikronClient for RegistrationClient {
|
||||||
|
async fn send_message(&self, _: &CommunicationValue) -> Result<(), OmikronError> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn await_response(
|
||||||
|
&self,
|
||||||
|
request: &CommunicationValue,
|
||||||
|
_: Duration,
|
||||||
|
) -> Result<CommunicationValue, OmikronError> {
|
||||||
|
assert!(request.is_type(CommunicationType::GetRegister));
|
||||||
|
Ok(self.response.clone().with_id(request.get_id()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reconnect(&self) -> Result<(), OmikronError> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn rotate_identity(&self) -> Result<(), OmikronError> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_connected(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validates_usernames_before_remote_registration() {
|
||||||
|
assert!(valid_username("alice"));
|
||||||
|
assert!(valid_username("fifteen_char_ok"));
|
||||||
|
assert!(!valid_username(""));
|
||||||
|
assert!(!valid_username("sixteen_chars_bad"));
|
||||||
|
assert!(!valid_username("path/name"));
|
||||||
|
assert!(!valid_username("line\nbreak"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn uses_user_id_allocated_by_omega() {
|
||||||
|
let client = RegistrationClient {
|
||||||
|
response: CommunicationValue::new(CommunicationType::GetRegister)
|
||||||
|
.add_typed_default(DataType::UserId, DataValue::SignedNumber(4_294_967_311))
|
||||||
|
.add_typed_default(
|
||||||
|
DataType::RegisterId,
|
||||||
|
DataValue::Str("00000000-0000-4000-8000-000000000001".into()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
request_user_id(&client).await.unwrap(),
|
||||||
|
(4_294_967_311, "00000000-0000-4000-8000-000000000001".into())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rejects_registration_response_without_a_positive_user_id() {
|
||||||
|
let client = RegistrationClient {
|
||||||
|
response: CommunicationValue::new(CommunicationType::GetRegister)
|
||||||
|
.add_typed_default(DataType::UserId, DataValue::SignedNumber(0)),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
request_user_id(&client).await,
|
||||||
|
Err(CreateUserError::InvalidResponse)
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
278
type-maps.yaml
278
type-maps.yaml
|
|
@ -1,278 +0,0 @@
|
||||||
protocol_version: "1.0"
|
|
||||||
|
|
||||||
# Note that markers 0 to 31 are reserved for default use, manually working with them is not recommended
|
|
||||||
|
|
||||||
# Fixed CommunicationType markers are:
|
|
||||||
# Error: 0
|
|
||||||
# ErrorParsing: 1
|
|
||||||
# ErrorBadVersion: 2
|
|
||||||
# Disconnect: 3
|
|
||||||
# Redirect: 4
|
|
||||||
# Shutdown: 5
|
|
||||||
# BadRequest: 6
|
|
||||||
# Unauthorized: 7
|
|
||||||
# Forbidden: 8
|
|
||||||
# NotFound: 9
|
|
||||||
# TooManyRequests: 10
|
|
||||||
# InternalServerError: 11
|
|
||||||
# BadGateway: 12
|
|
||||||
# ServiceUnavailable: 13
|
|
||||||
# GatewayTimeout: 14
|
|
||||||
# Identification: 15
|
|
||||||
# IdentificationResponse: 16
|
|
||||||
# Register: 17
|
|
||||||
# RegisterResponse: 18
|
|
||||||
# Ping: 19
|
|
||||||
# Pong: 20
|
|
||||||
|
|
||||||
# Fixed Data Type markers are:
|
|
||||||
# Error: 0
|
|
||||||
# ErrorParsing: 1
|
|
||||||
# ErrorMessage: 2
|
|
||||||
# Version: 3
|
|
||||||
# Description: 4
|
|
||||||
# Timestamp: 5
|
|
||||||
# Id: 6
|
|
||||||
# ClientNonce: 7
|
|
||||||
# ServerNonce: 8
|
|
||||||
# PublicKeys: 9
|
|
||||||
# Signature: 10
|
|
||||||
# Connected: 11
|
|
||||||
|
|
||||||
type_maps:
|
|
||||||
"1.0":
|
|
||||||
CommunicationTypes:
|
|
||||||
ErrorProtocol: 33
|
|
||||||
ErrorAnonymous: 34
|
|
||||||
ErrorInternal: 35
|
|
||||||
ErrorInvalidData: 36
|
|
||||||
ErrorInvalidUserId: 37
|
|
||||||
ErrorInvalidOmikronId: 38
|
|
||||||
ErrorNotFound: 39
|
|
||||||
ErrorNotAuthenticated: 40
|
|
||||||
ErrorNoIota: 41
|
|
||||||
ErrorInvalidChallenge: 42
|
|
||||||
ErrorInvalidSecret: 43
|
|
||||||
ErrorInvalidPrivateKey: 44
|
|
||||||
ErrorInvalidPublicKey: 45
|
|
||||||
ErrorNoUserId: 46
|
|
||||||
ErrorNoCallId: 47
|
|
||||||
ErrorInvalidCallId: 48
|
|
||||||
Success: 49
|
|
||||||
ShortenLink: 50
|
|
||||||
SettingsSave: 51
|
|
||||||
SettingsLoad: 52
|
|
||||||
SettingsList: 53
|
|
||||||
GlobalSettingsSave: 54
|
|
||||||
GlobalSettingsLoad: 55
|
|
||||||
Message: 56
|
|
||||||
MessageState: 57
|
|
||||||
MessageSend: 58
|
|
||||||
MessageLive: 59
|
|
||||||
MessageOtherIota: 60
|
|
||||||
MessageChunk: 61
|
|
||||||
MessageGet: 143
|
|
||||||
MessagesGet: 62
|
|
||||||
PushNotification: 63
|
|
||||||
ReadNotification: 64
|
|
||||||
GetNotifications: 65
|
|
||||||
TauriIdentification: 66
|
|
||||||
ChangeConfirm: 67
|
|
||||||
ConfirmReceive: 68
|
|
||||||
ConfirmRead: 69
|
|
||||||
GetChats: 70
|
|
||||||
GetStates: 71
|
|
||||||
AddCommunity: 72
|
|
||||||
RemoveCommunity: 73
|
|
||||||
GetCommunities: 74
|
|
||||||
RegisterIota: 81
|
|
||||||
RegisterIotaSuccess: 82
|
|
||||||
AddConversation: 85
|
|
||||||
SendChat: 86
|
|
||||||
ClientChanged: 87
|
|
||||||
ClientConnected: 88
|
|
||||||
ClientDisconnected: 89
|
|
||||||
ClientClosed: 90
|
|
||||||
PublicKey: 91
|
|
||||||
PrivateKey: 92
|
|
||||||
WebrtcSdp: 93
|
|
||||||
WebrtcIce: 94
|
|
||||||
StartStream: 95
|
|
||||||
EndStream: 96
|
|
||||||
WatchStream: 97
|
|
||||||
CallToken: 98
|
|
||||||
CallInvite: 99
|
|
||||||
CallDisconnectUser: 100
|
|
||||||
CallTimeoutUser: 101
|
|
||||||
CallSetAnonymousJoining: 102
|
|
||||||
CallData: 103
|
|
||||||
EndCall: 104
|
|
||||||
Function: 105
|
|
||||||
Update: 106
|
|
||||||
CreateUser: 107
|
|
||||||
RhoUpdate: 108
|
|
||||||
UserConnected: 109
|
|
||||||
UserDisconnected: 110
|
|
||||||
IotaConnected: 111
|
|
||||||
IotaDisconnected: 112
|
|
||||||
SyncClientIotaStatus: 113
|
|
||||||
GetUserData: 114
|
|
||||||
GetIotaData: 115
|
|
||||||
IotaUserData: 116
|
|
||||||
ChangeUserData: 117
|
|
||||||
ChangeIotaData: 118
|
|
||||||
GetRegister: 119
|
|
||||||
CompleteRegisterUser: 120
|
|
||||||
CompleteRegisterIota: 121
|
|
||||||
DeleteUser: 122
|
|
||||||
DeleteIota: 123
|
|
||||||
StartRegister: 124
|
|
||||||
CompleteRegister: 125
|
|
||||||
GetApp: 126
|
|
||||||
CreateApp: 127
|
|
||||||
DeleteApp: 128
|
|
||||||
SaveAppData: 129
|
|
||||||
LoadAppData: 130
|
|
||||||
AppIdentification: 131
|
|
||||||
AppChallenge: 132
|
|
||||||
AppChallengeResponse: 133
|
|
||||||
AppIdentificationResponse: 134
|
|
||||||
LoadTxtRecord: 135
|
|
||||||
ErrorNotSet: 136
|
|
||||||
SetChatSecret: 139
|
|
||||||
GetChatSecret: 140
|
|
||||||
ChatSecretResponse: 141
|
|
||||||
ChatSecretForward: 142
|
|
||||||
MessageEditLive: 144
|
|
||||||
MessageEdit: 145
|
|
||||||
MessageReactionAdd: 146
|
|
||||||
MessageReactionRemove: 147
|
|
||||||
MessageReactionLive: 148
|
|
||||||
MessageDeleteLive: 150
|
|
||||||
ClientStateSync: 151
|
|
||||||
ClientStateAck: 152
|
|
||||||
StateSubscribe: 153
|
|
||||||
DataTypes:
|
|
||||||
ErrorType: 32
|
|
||||||
ErrorProtocol: 33
|
|
||||||
AcceptedIds: 34
|
|
||||||
Uuid: 35
|
|
||||||
RegisterId: 36
|
|
||||||
Link: 37
|
|
||||||
Settings: 38
|
|
||||||
SettingsName: 39
|
|
||||||
ChatPartnerId: 40
|
|
||||||
ChatPartnerName: 41
|
|
||||||
IotaId: 42
|
|
||||||
UserId: 43
|
|
||||||
UserIds: 44
|
|
||||||
IotaIds: 45
|
|
||||||
UserState: 46
|
|
||||||
UserStates: 47
|
|
||||||
UserPings: 48
|
|
||||||
CallState: 49
|
|
||||||
ScreenShare: 50
|
|
||||||
PrivateKeyHash: 51
|
|
||||||
# Accepted: 52 now part of default MTP
|
|
||||||
AcceptedProfiles: 53
|
|
||||||
DeniedProfiles: 54
|
|
||||||
Content: 55
|
|
||||||
Messages: 56
|
|
||||||
Notifications: 57
|
|
||||||
SendTime: 58
|
|
||||||
GetTime: 59
|
|
||||||
GetVariant: 60
|
|
||||||
SharedSecretOwn: 61
|
|
||||||
SharedSecretOther: 62
|
|
||||||
SharedSecretSign: 63
|
|
||||||
SharedSecret: 64
|
|
||||||
CallId: 65
|
|
||||||
CallToken: 66
|
|
||||||
CallSecret: 67
|
|
||||||
Untill: 68
|
|
||||||
Enabled: 69
|
|
||||||
StartDate: 70
|
|
||||||
EndDate: 71
|
|
||||||
ReceiverId: 72
|
|
||||||
SenderId: 73
|
|
||||||
Signed: 75
|
|
||||||
Message: 76
|
|
||||||
MessageState: 77
|
|
||||||
LastPing: 78
|
|
||||||
PingIota: 79
|
|
||||||
PingClients: 80
|
|
||||||
Matches: 81
|
|
||||||
Omikron: 82
|
|
||||||
Offset: 83
|
|
||||||
Amount: 84
|
|
||||||
Position: 85
|
|
||||||
Name: 86
|
|
||||||
Path: 87
|
|
||||||
Codec: 88
|
|
||||||
Function: 89
|
|
||||||
Payload: 90
|
|
||||||
Result: 91
|
|
||||||
Interactables: 92
|
|
||||||
WantToWatch: 93
|
|
||||||
Watcher: 94
|
|
||||||
CreatedAt: 95
|
|
||||||
Username: 96
|
|
||||||
Display: 97
|
|
||||||
Avatar: 98
|
|
||||||
About: 99
|
|
||||||
Status: 100
|
|
||||||
PublicKey: 101
|
|
||||||
SubLevel: 102
|
|
||||||
SubEnd: 103
|
|
||||||
CommunityAddress: 104
|
|
||||||
CommunityTitle: 106
|
|
||||||
Communities: 107
|
|
||||||
RhoConnections: 108
|
|
||||||
User: 109
|
|
||||||
OnlineStatus: 110
|
|
||||||
OmikronId: 111
|
|
||||||
OmikronConnections: 112
|
|
||||||
ResetToken: 113
|
|
||||||
NewToken: 114
|
|
||||||
CallInvited: 115
|
|
||||||
CallMembers: 116
|
|
||||||
Calls: 117
|
|
||||||
Timeout: 118
|
|
||||||
HasAdmin: 119
|
|
||||||
LastMessageAt: 120
|
|
||||||
Height: 121
|
|
||||||
SentBySelf: 122
|
|
||||||
SessionId: 123
|
|
||||||
Contacts: 124
|
|
||||||
LastMessage: 125
|
|
||||||
AppIdentifier: 127
|
|
||||||
AppPrivateKey: 128
|
|
||||||
AppPublicKey: 129
|
|
||||||
AppSession: 130
|
|
||||||
AppData: 131
|
|
||||||
TauriToken: 132
|
|
||||||
Challenge: 133
|
|
||||||
EncryptedPayload: 134
|
|
||||||
SecurePayload: 135
|
|
||||||
DeviceId: 136
|
|
||||||
ClientId: 137
|
|
||||||
SecretId: 142
|
|
||||||
VersionNumber: 143
|
|
||||||
EncryptedSecret: 144
|
|
||||||
WrappingScheme: 146
|
|
||||||
UpdatedAt: 147
|
|
||||||
ChatId: 148
|
|
||||||
KemCiphertext: 149
|
|
||||||
SenderUserId: 152
|
|
||||||
RecipientUserId: 153
|
|
||||||
Recipients: 154
|
|
||||||
Edited: 155
|
|
||||||
Reactions: 156
|
|
||||||
Reaction: 157
|
|
||||||
ReplyId: 158
|
|
||||||
CacheValid: 159
|
|
||||||
CacheSchemaVersion: 160
|
|
||||||
SyncMode: 161
|
|
||||||
MessageId: 162
|
|
||||||
DeletedMessageIds: 163
|
|
||||||
DeletedContactIds: 164
|
|
||||||
Loading…
Reference in a new issue