[Updt] Mtp 0.3.0
This commit is contained in:
parent
e1dd86ec02
commit
ad8555bc6e
45 changed files with 2019 additions and 1441 deletions
|
|
@ -14,7 +14,6 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
|
|||
"client",
|
||||
"crypto",
|
||||
"files",
|
||||
"raw",
|
||||
] }
|
||||
|
||||
dashmap = "6.2.1"
|
||||
|
|
@ -24,8 +23,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 = "*" }
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ use iota_util::crypto_util::{self};
|
|||
use mtp::client::{Client, ClientConfig, MTPConnection, Policy, SendMode, Sender};
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||
use mtp::crypto::{Keyring, PublicKeyBundle};
|
||||
use rand_core::RngCore;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{
|
||||
Arc, LazyLock,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::{Mutex, RwLock, Semaphore, oneshot, watch};
|
||||
use tokio::task::JoinHandle;
|
||||
|
|
@ -38,6 +38,9 @@ use iota_util::route_target::RouteTarget;
|
|||
// ============================================================================
|
||||
|
||||
const IOTA_KEYRING_PATH: &str = "iota.mk";
|
||||
const IDENTITY_SECRET_ENV: &str = "IOTA_IDENTITY_SECRET";
|
||||
const IDENTITY_SECRET_FILE_ENV: &str = "IOTA_IDENTITY_SECRET_FILE";
|
||||
const SYSTEMD_IDENTITY_CREDENTIAL: &str = "iota-identity";
|
||||
static IDENTITY_PATH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
|
||||
|
||||
/// Must be called by the daemon before any Omikron connection is attempted.
|
||||
|
|
@ -60,7 +63,173 @@ const TASK_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
|
|||
const TASK_MAX_AGE: Duration = Duration::from_secs(60);
|
||||
const MAX_CONCURRENT_HANDLERS: usize = 20;
|
||||
const RELAY_RETENTION_MILLIS: i64 = 30 * 24 * 60 * 60 * 1000;
|
||||
static NEXT_RELAY_FRAME_ID: AtomicU32 = AtomicU32::new(1);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum IdentityError {
|
||||
Storage(mtp::files::FileError),
|
||||
Directory(std::io::Error),
|
||||
Secret(String),
|
||||
InvalidLegacyIdentity,
|
||||
Verification(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IdentityError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Storage(error) => write!(f, "identity storage error: {error}"),
|
||||
Self::Directory(error) => write!(f, "unable to create identity directory: {error}"),
|
||||
Self::Secret(error) => write!(f, "unable to load identity secret: {error}"),
|
||||
Self::InvalidLegacyIdentity => f.write_str("legacy identity is invalid"),
|
||||
Self::Verification(error) => {
|
||||
write!(f, "persisted identity could not be verified: {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IdentityError {}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ConnectionAttemptResult {
|
||||
became_healthy: bool,
|
||||
}
|
||||
|
||||
fn jittered_reconnect_delay(delay: Duration) -> Duration {
|
||||
let ceiling_ms = u64::try_from(MAX_RECONNECT_DELAY.as_millis())
|
||||
.expect("reconnect ceiling must fit in milliseconds");
|
||||
let base_ms = u64::try_from(delay.as_millis().min(u128::from(ceiling_ms)))
|
||||
.expect("bounded reconnect delay must fit in milliseconds");
|
||||
let jitter_span = base_ms / 5;
|
||||
if jitter_span == 0 {
|
||||
return Duration::from_millis(base_ms);
|
||||
}
|
||||
|
||||
let mut rng = rand_core::OsRng;
|
||||
let range = jitter_span.saturating_mul(2).saturating_add(1);
|
||||
let offset = (rng.next_u64() % range) as i128 - jitter_span as i128;
|
||||
let jittered = (base_ms as i128 + offset).clamp(0, i128::from(ceiling_ms));
|
||||
Duration::from_millis(u64::try_from(jittered).expect("bounded jitter must be non-negative"))
|
||||
}
|
||||
|
||||
fn wire_user_id(user_id: i64) -> u64 {
|
||||
u64::try_from(user_id).expect("validated user ID is non-negative")
|
||||
}
|
||||
|
||||
fn load_identity_secret() -> Result<Vec<u8>, IdentityError> {
|
||||
if let Some(path) = env::var_os(IDENTITY_SECRET_FILE_ENV) {
|
||||
let path = PathBuf::from(path);
|
||||
let mut secret = fs::read(&path)
|
||||
.map_err(|error| IdentityError::Secret(format!("{}: {error}", path.display())))?;
|
||||
while matches!(secret.last(), Some(b'\n' | b'\r')) {
|
||||
secret.pop();
|
||||
}
|
||||
if secret.is_empty() {
|
||||
return Err(IdentityError::Secret(format!(
|
||||
"{} is empty",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
return Ok(secret);
|
||||
}
|
||||
|
||||
if let Ok(secret) = env::var(IDENTITY_SECRET_ENV) {
|
||||
if secret.is_empty() {
|
||||
return Err(IdentityError::Secret(format!(
|
||||
"{IDENTITY_SECRET_ENV} is empty"
|
||||
)));
|
||||
}
|
||||
return Ok(secret.into_bytes());
|
||||
}
|
||||
|
||||
if let Ok(credentials_dir) = env::var("CREDENTIALS_DIRECTORY") {
|
||||
let path = Path::new(&credentials_dir).join(SYSTEMD_IDENTITY_CREDENTIAL);
|
||||
let mut secret = fs::read(&path)
|
||||
.map_err(|error| IdentityError::Secret(format!("{}: {error}", path.display())))?;
|
||||
while matches!(secret.last(), Some(b'\n' | b'\r')) {
|
||||
secret.pop();
|
||||
}
|
||||
if secret.is_empty() {
|
||||
return Err(IdentityError::Secret(format!(
|
||||
"{} is empty",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
return Ok(secret);
|
||||
}
|
||||
|
||||
Err(IdentityError::Secret(format!(
|
||||
"set {IDENTITY_SECRET_FILE_ENV}, {IDENTITY_SECRET_ENV}, or a systemd identity credential"
|
||||
)))
|
||||
}
|
||||
|
||||
fn load_legacy_raw_keyring(path: &Path) -> Result<Keyring, IdentityError> {
|
||||
let bytes = fs::read(path).map_err(|error| IdentityError::Storage(error.into()))?;
|
||||
if bytes.len() < 5 || bytes[..4] != *b"MTMK" || bytes[4] != 1 {
|
||||
return Err(IdentityError::InvalidLegacyIdentity);
|
||||
}
|
||||
Keyring::from_bytes(&bytes[5..]).map_err(|_| IdentityError::InvalidLegacyIdentity)
|
||||
}
|
||||
|
||||
fn save_protected_keyring_verified(
|
||||
keyring: &Keyring,
|
||||
path: &Path,
|
||||
passphrase: &[u8],
|
||||
) -> Result<(), IdentityError> {
|
||||
mtp::files::save_keyring(keyring, path, passphrase).map_err(IdentityError::Storage)?;
|
||||
let persisted = mtp::files::load_keyring(path, passphrase).map_err(IdentityError::Storage)?;
|
||||
let expected = keyring
|
||||
.try_to_bytes()
|
||||
.map_err(|error| IdentityError::Verification(error.to_string()))?;
|
||||
let actual = persisted
|
||||
.try_to_bytes()
|
||||
.map_err(|error| IdentityError::Verification(error.to_string()))?;
|
||||
if expected != actual {
|
||||
return Err(IdentityError::Verification(
|
||||
"persisted keyring differs from the requested identity".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_or_migrate_keyring_at(
|
||||
path: &Path,
|
||||
legacy: Option<String>,
|
||||
passphrase: &[u8],
|
||||
) -> Result<Keyring, IdentityError> {
|
||||
if let Some(parent) = path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
{
|
||||
fs::create_dir_all(parent).map_err(IdentityError::Directory)?;
|
||||
}
|
||||
|
||||
match mtp::files::load_keyring(path, passphrase) {
|
||||
Ok(keyring) => return Ok(keyring),
|
||||
Err(mtp::files::FileError::Io(error)) if error.kind() == ErrorKind::NotFound => {}
|
||||
Err(mtp::files::FileError::UnprotectedKeyring) => {
|
||||
let keyring = load_legacy_raw_keyring(path)?;
|
||||
save_protected_keyring_verified(&keyring, path, passphrase)?;
|
||||
return Ok(keyring);
|
||||
}
|
||||
Err(error) => return Err(IdentityError::Storage(error)),
|
||||
}
|
||||
|
||||
let keyring = match legacy {
|
||||
Some(encoded) => {
|
||||
keyring_from_base64(&encoded).ok_or(IdentityError::InvalidLegacyIdentity)?
|
||||
}
|
||||
None => {
|
||||
log!(
|
||||
"No existing Iota identity found at {}; generating a new identity",
|
||||
path.display()
|
||||
);
|
||||
crypto_helper::generate_keyring()
|
||||
}
|
||||
};
|
||||
|
||||
save_protected_keyring_verified(&keyring, path, passphrase)?;
|
||||
Ok(keyring)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Waiting Task System
|
||||
|
|
@ -128,14 +297,6 @@ pub struct OmikronConnection {
|
|||
pub(crate) app: Arc<std::sync::Mutex<AppState>>,
|
||||
}
|
||||
|
||||
fn ensure_relay_frame_id(frame: CommunicationValue) -> CommunicationValue {
|
||||
if frame.id().is_some_and(|id| id != 0) {
|
||||
return frame;
|
||||
}
|
||||
let id = NEXT_RELAY_FRAME_ID.fetch_add(1, Ordering::Relaxed).max(1);
|
||||
frame.with_id(id)
|
||||
}
|
||||
|
||||
impl OmikronConnection {
|
||||
pub fn new(active_tasks: Arc<DashSet<String>>, app: Arc<std::sync::Mutex<AppState>>) -> Self {
|
||||
Self::with_cancellation(CancellationToken::new(), active_tasks, app)
|
||||
|
|
@ -248,29 +409,29 @@ impl OmikronConnection {
|
|||
break;
|
||||
}
|
||||
|
||||
match self.clone().connect_once().await {
|
||||
Ok(()) => {
|
||||
if *self.reconnect_on_close.read().await {
|
||||
log!("Connection lost, reconnecting in {:?}...", reconnect_delay);
|
||||
} else {
|
||||
let retry_reason = match self.clone().connect_once().await {
|
||||
Ok(result) => {
|
||||
if result.became_healthy {
|
||||
reconnect_delay = RECONNECT_DELAY;
|
||||
}
|
||||
if !*self.reconnect_on_close.read().await {
|
||||
break;
|
||||
}
|
||||
"Connection lost".to_string()
|
||||
}
|
||||
Err(e) => {
|
||||
if self.auth_failure.read().await.is_some() {
|
||||
log!("Authentication failed, stopping reconnection: {}", e);
|
||||
break;
|
||||
}
|
||||
log!(
|
||||
"Connection failed: {}, retrying in {:?}...",
|
||||
e,
|
||||
reconnect_delay
|
||||
);
|
||||
format!("Connection failed: {e}")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let delay = jittered_reconnect_delay(reconnect_delay);
|
||||
log!("{}, retrying in {:?}...", retry_reason, delay);
|
||||
tokio::select! {
|
||||
_ = sleep(reconnect_delay) => {}
|
||||
_ = sleep(delay) => {}
|
||||
_ = shutdown_rx.changed() => {
|
||||
if *shutdown_rx.borrow() {
|
||||
break;
|
||||
|
|
@ -282,11 +443,16 @@ impl OmikronConnection {
|
|||
}
|
||||
}
|
||||
|
||||
async fn connect_once(self: Arc<Self>) -> Result<(), String> {
|
||||
async fn connect_once(self: Arc<Self>) -> Result<ConnectionAttemptResult, String> {
|
||||
self.set_state(ConnectionState::Connecting).await;
|
||||
log_t!("omikron_connecting");
|
||||
|
||||
let keyring = Arc::new(self.load_or_migrate_keyring().await);
|
||||
let identity_secret = load_identity_secret().map_err(|error| error.to_string())?;
|
||||
let keyring = Arc::new(
|
||||
self.load_or_migrate_keyring(&identity_secret)
|
||||
.await
|
||||
.map_err(|error| format!("Iota identity initialization failed: {error}"))?,
|
||||
);
|
||||
*self.keyring.write().await = Some(keyring.clone());
|
||||
|
||||
let existing_iota_id = CONFIG.load().iota_id;
|
||||
|
|
@ -298,29 +464,21 @@ impl OmikronConnection {
|
|||
|
||||
log!("Connecting to Omikron at {}", addr_str);
|
||||
|
||||
let policy = Policy::default()
|
||||
.with_send_mode(SendMode::SingleStreamPerMessage)
|
||||
.with_timeouts(
|
||||
Duration::from_millis(2_000),
|
||||
Duration::from_millis(2_000),
|
||||
Duration::from_millis(30_000),
|
||||
)
|
||||
.with_keep_alive(Some(Duration::from_secs(6)))
|
||||
.with_receiver_queue_capacity(1000)
|
||||
.with_max_concurrent_stream_tasks(10)
|
||||
.with_persistent_stream_retries(5, Duration::from_secs(5));
|
||||
let client_config = ClientConfig::new(&addr_str)
|
||||
.with_description("iota")
|
||||
.with_policy(Policy {
|
||||
send_mode: SendMode::SingleStreamPerMessage,
|
||||
max_message_size: 1_000_000_000,
|
||||
handshake_max_message_size: 1_000_000_000,
|
||||
close_frame_len: u32::MAX,
|
||||
application_close_code: 0,
|
||||
open_stream_timeout: Duration::from_millis(2_000),
|
||||
write_timeout: Duration::from_millis(2_000),
|
||||
accept_stream_timeout: Duration::from_millis(10_000),
|
||||
read_timeout: Duration::from_millis(30_000),
|
||||
keep_alive_interval: Some(Duration::from_secs(6)),
|
||||
max_idle_timeout: None,
|
||||
force_close_delay: Duration::from_millis(300),
|
||||
receiver_queue_capacity: 1000,
|
||||
max_concurrent_stream_tasks: 10,
|
||||
persistent_stream_max_retries: 5,
|
||||
persistent_stream_retry_backoff: Duration::from_secs(5),
|
||||
max_frames_per_stream: None,
|
||||
})
|
||||
.with_ping_interval(MAINTENANCE_INTERVAL)
|
||||
.with_max_missed_pings(0);
|
||||
.with_policy(policy)
|
||||
.with_ping_interval(MAINTENANCE_INTERVAL);
|
||||
|
||||
let connection = match Client::auth_connect_or_register(
|
||||
client_config,
|
||||
|
|
@ -389,13 +547,9 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
if *self.reconnect_on_close.read().await {
|
||||
Err("Connection closed, will reconnect".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Ok(()) => Ok(ConnectionAttemptResult {
|
||||
became_healthy: true,
|
||||
}),
|
||||
Err(e) => Err(format!("Read loop error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
|
@ -404,41 +558,8 @@ impl OmikronConnection {
|
|||
// Identity (own Keyring, migrated from the legacy base64-in-config format)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* `iota.mk` is now the source of truth for this Iota's identity. A
|
||||
* pre-existing base64 keyring in config.json (from before the MTP auth
|
||||
* migration) is imported once so already-registered Iotas keep their
|
||||
* identity, and mirrored back into config.json for older code paths
|
||||
* that still read it directly.
|
||||
*/
|
||||
async fn load_or_migrate_keyring(&self) -> Keyring {
|
||||
let path = identity_path();
|
||||
if let Ok(kr) = mtp::files::load_keyring_raw(path) {
|
||||
return kr;
|
||||
}
|
||||
|
||||
let legacy = CONFIG.load().keyring.clone();
|
||||
let keyring = legacy
|
||||
.and_then(|b64| keyring_from_base64(&b64))
|
||||
.unwrap_or_else(|| {
|
||||
log!(
|
||||
"WARNING: No existing keyring found. Neither {} nor config.json \
|
||||
contain a keyring; generating a new identity. If you already had \
|
||||
an Iota identity, restore {} from a backup to avoid losing access.",
|
||||
path.display(),
|
||||
path.display()
|
||||
);
|
||||
crypto_helper::generate_keyring()
|
||||
});
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
if let Err(e) = mtp::files::save_keyring_raw(&keyring, path) {
|
||||
log!("Failed to persist {}: {}", path.display(), e);
|
||||
}
|
||||
|
||||
keyring
|
||||
async fn load_or_migrate_keyring(&self, passphrase: &[u8]) -> Result<Keyring, IdentityError> {
|
||||
load_or_migrate_keyring_at(identity_path(), CONFIG.load().keyring.clone(), passphrase)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
|
@ -503,17 +624,24 @@ impl OmikronConnection {
|
|||
};
|
||||
|
||||
let (host, port, public_key) = if let Some(endpoint) = discovered {
|
||||
let discovered_key_bytes = endpoint.public_key.try_as_bytes().map_err(|error| {
|
||||
format!("Failed to serialize discovered Omikron public key: {error}")
|
||||
})?;
|
||||
match &cached_key {
|
||||
Some(cached) if cached.as_bytes() != endpoint.public_key.as_bytes() => {
|
||||
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
|
||||
);
|
||||
Some(cached) => {
|
||||
let cached_key_bytes = cached.try_as_bytes().map_err(|error| {
|
||||
format!("Failed to serialize cached Omikron public key: {error}")
|
||||
})?;
|
||||
if cached_key_bytes != discovered_key_bytes {
|
||||
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
|
||||
);
|
||||
}
|
||||
(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,
|
||||
|
|
@ -563,7 +691,13 @@ impl OmikronConnection {
|
|||
});
|
||||
continue;
|
||||
}
|
||||
let msg_id = cv.get_id();
|
||||
let Some(msg_id) = cv.id() else {
|
||||
let self_clone = self.clone();
|
||||
tokio::spawn(async move {
|
||||
self_clone.handle_message_impl(cv).await;
|
||||
});
|
||||
continue;
|
||||
};
|
||||
if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) {
|
||||
if (task.task)(cv.clone()) {
|
||||
continue;
|
||||
|
|
@ -613,7 +747,7 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
if let Some(ping) = connection.get_ping() {
|
||||
let ping_ms = ping.as_millis() as i64;
|
||||
let ping_ms = i64::try_from(ping.as_millis()).unwrap_or(i64::MAX);
|
||||
*self.last_ping.lock().await = ping_ms;
|
||||
self.app.lock().unwrap().push_ping_val(ping_ms as f64);
|
||||
}
|
||||
|
|
@ -631,7 +765,10 @@ impl OmikronConnection {
|
|||
&self,
|
||||
signer_id: u64,
|
||||
) -> Result<Vec<PublicKeyBundle>, RelayValidationError> {
|
||||
if let Some(user) = iota_storage::users::user_manager::get_user(signer_id as i64) {
|
||||
let signer_id_i64 = i64::try_from(signer_id).map_err(|_| {
|
||||
RelayValidationError::KeyLookup("signer ID exceeds local storage range".into())
|
||||
})?;
|
||||
if let Some(user) = iota_storage::users::user_manager::get_user(signer_id_i64) {
|
||||
let key = iota_util::crypto_helper::public_key_bundle_from_base64(&user.public_key)
|
||||
.ok_or_else(|| {
|
||||
RelayValidationError::KeyLookup("stored user key is invalid".into())
|
||||
|
|
@ -641,7 +778,7 @@ impl OmikronConnection {
|
|||
|
||||
let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default(
|
||||
DataType::UserId,
|
||||
DataValue::UnsignedNumber(signer_id as u128),
|
||||
DataValue::UnsignedNumber(u128::from(signer_id)),
|
||||
);
|
||||
let response = self
|
||||
.await_response(&request, Some(Duration::from_secs(10)))
|
||||
|
|
@ -663,15 +800,19 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
pub async fn hosting_iota_for_user(&self, user_id: u64) -> Result<u64, String> {
|
||||
if iota_storage::users::user_manager::get_user(user_id as i64).is_some() {
|
||||
let user_id_i64 = i64::try_from(user_id)
|
||||
.map_err(|_| "user ID exceeds local storage range".to_string())?;
|
||||
if iota_storage::users::user_manager::get_user(user_id_i64).is_some() {
|
||||
return CONFIG
|
||||
.load()
|
||||
.iota_id
|
||||
.ok_or_else(|| "Iota identity is not configured".into());
|
||||
}
|
||||
|
||||
let request = CommunicationValue::new(CommunicationType::GetUserData)
|
||||
.add_typed_default(DataType::UserId, DataValue::UnsignedNumber(user_id as u128));
|
||||
let request = CommunicationValue::new(CommunicationType::GetUserData).add_typed_default(
|
||||
DataType::UserId,
|
||||
DataValue::UnsignedNumber(u128::from(user_id)),
|
||||
);
|
||||
let response = self
|
||||
.await_response(&request, Some(Duration::from_secs(10)))
|
||||
.await?;
|
||||
|
|
@ -693,17 +834,19 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
async fn handle_relay(self: Arc<Self>, frame: CommunicationValue) {
|
||||
let frame = ensure_relay_frame_id(frame);
|
||||
let incoming_frame_id = frame.id();
|
||||
let Some(incoming_frame_id) = frame.id() else {
|
||||
log!("Rejecting Relay without a message id");
|
||||
return;
|
||||
};
|
||||
let Some(local_iota_id) = CONFIG.load().iota_id else {
|
||||
log!("Rejecting Relay because this Iota has no registered identity");
|
||||
self.send_relay_response(incoming_frame_id, CommunicationType::ErrorInvalidData)
|
||||
self.send_relay_response(Some(incoming_frame_id), CommunicationType::ErrorInvalidData)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
let Some(keyring) = self.keyring.read().await.as_ref().cloned() else {
|
||||
log!("Rejecting Relay because the Iota keyring is unavailable");
|
||||
self.send_relay_response(incoming_frame_id, CommunicationType::ErrorInternal)
|
||||
self.send_relay_response(Some(incoming_frame_id), CommunicationType::ErrorInternal)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
|
|
@ -725,24 +868,29 @@ impl OmikronConnection {
|
|||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
log!("Relay metadata verification failed: {}", error);
|
||||
self.send_relay_response(incoming_frame_id, CommunicationType::ErrorInvalidData)
|
||||
.await;
|
||||
self.send_relay_response(
|
||||
Some(incoming_frame_id),
|
||||
CommunicationType::ErrorInvalidData,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let signer_is_local =
|
||||
iota_storage::users::user_manager::get_user(verified.context.signer_id as i64)
|
||||
.is_some();
|
||||
let recipient_is_local =
|
||||
iota_storage::users::user_manager::get_user(verified.context.final_recipient_id as i64)
|
||||
.is_some();
|
||||
let signer_is_local = i64::try_from(verified.context.signer_id)
|
||||
.ok()
|
||||
.and_then(iota_storage::users::user_manager::get_user)
|
||||
.is_some();
|
||||
let recipient_is_local = i64::try_from(verified.context.final_recipient_id)
|
||||
.ok()
|
||||
.and_then(iota_storage::users::user_manager::get_user)
|
||||
.is_some();
|
||||
if !signer_is_local && !recipient_is_local {
|
||||
log!(
|
||||
"Rejecting Relay with no local origin or destination: signer {}, recipient {}",
|
||||
verified.context.signer_id,
|
||||
verified.context.final_recipient_id,
|
||||
);
|
||||
self.send_relay_response(frame.id(), CommunicationType::ErrorInvalidData)
|
||||
self.send_relay_response(Some(incoming_frame_id), CommunicationType::ErrorInvalidData)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
|
@ -760,7 +908,7 @@ impl OmikronConnection {
|
|||
}
|
||||
};
|
||||
let type_map_version = verified.context.type_map.version.to_string();
|
||||
let frame_id = frame.id().unwrap_or_default();
|
||||
let frame_id = incoming_frame_id;
|
||||
let reservation = match relay_replay::reserve(
|
||||
verified.context.signer_id,
|
||||
&verified.context.message_id,
|
||||
|
|
@ -927,7 +1075,7 @@ impl OmikronConnection {
|
|||
RouteTarget::User(destination),
|
||||
&bytes,
|
||||
now_millis_i64(),
|
||||
forwarded.id().unwrap_or_default(),
|
||||
frame_id,
|
||||
&type_map_version,
|
||||
) {
|
||||
log!("Relay could not be queued for client delivery: {}", error);
|
||||
|
|
@ -1069,7 +1217,7 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Message Handling — Dispatch
|
||||
// Message Handling - Dispatch
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
pub async fn handle_message(self: Arc<Self>, cv: CommunicationValue) {
|
||||
|
|
@ -1096,7 +1244,10 @@ impl OmikronConnection {
|
|||
}
|
||||
}
|
||||
|
||||
let msg_id = cv.get_id();
|
||||
let Some(msg_id) = cv.id() else {
|
||||
self.handle_message_impl(cv).await;
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some((_, task)) = WAITING_TASKS.remove(&msg_id) {
|
||||
if (task.task)(cv.clone()) {
|
||||
|
|
@ -1112,6 +1263,12 @@ impl OmikronConnection {
|
|||
self.handle_relay(cv).await;
|
||||
return;
|
||||
}
|
||||
if cv.require_id().is_err() {
|
||||
let _ = self
|
||||
.send_message(&error_response(&cv, CommunicationType::ErrorInvalidData))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
iota_connection::relay::message_security_class(&cv),
|
||||
|
|
@ -1191,7 +1348,7 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
let acknowledgement = CommunicationValue::new(CommunicationType::EraseHostedUserDataAck)
|
||||
.with_id(cv.get_id())
|
||||
.with_request_id(cv)
|
||||
.add_typed_default(DataType::UserId, DataValue::SignedNumber(user_id.into()));
|
||||
let _ = self.send_message(&acknowledgement).await;
|
||||
}
|
||||
|
|
@ -1203,7 +1360,15 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
async fn handle_app_identification(self: Arc<Self>, cv: &CommunicationValue) {
|
||||
let sender_id = cv.get_sender();
|
||||
let sender_id = match cv.require_sender() {
|
||||
Ok(sender_id) => sender_id,
|
||||
Err(_) => {
|
||||
let _ = self
|
||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let app_identifier = cv
|
||||
.get_data(DataType::AppIdentifier)
|
||||
.as_str()
|
||||
|
|
@ -1214,7 +1379,12 @@ impl OmikronConnection {
|
|||
.as_str()
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let user_id = cv.get_data(DataType::UserId).as_number().unwrap_or(0) as i64;
|
||||
let Some(user_id) = data_i64(cv, DataType::UserId).filter(|id| *id > 0) else {
|
||||
let _ = self
|
||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
|
||||
let mut trusted = false;
|
||||
if let Some(user) = iota_storage::users::user_manager::get_user(user_id) {
|
||||
|
|
@ -1235,7 +1405,8 @@ impl OmikronConnection {
|
|||
if let Some(app_pub_bundle) =
|
||||
iota_util::crypto_helper::public_key_bundle_from_base64(&app_public_key)
|
||||
{
|
||||
if let Ok(keyring) = mtp::files::load_keyring_raw(identity_path()) {
|
||||
let keyring = self.keyring.read().await.as_ref().cloned();
|
||||
if let Some(keyring) = keyring {
|
||||
if let Ok(encrypted_challenge) =
|
||||
crypto_util::encrypt_challenge(&challenge, &app_pub_bundle)
|
||||
{
|
||||
|
|
@ -1243,7 +1414,7 @@ impl OmikronConnection {
|
|||
let pub_k_b64 = crypto_helper::public_key_bundle_to_base64(&bundle);
|
||||
|
||||
let res = CommunicationValue::new(CommunicationType::AppChallenge)
|
||||
.with_id(cv.get_id())
|
||||
.with_request_id(cv)
|
||||
.with_receiver(sender_id)
|
||||
.add_typed_default(DataType::PublicKey, DataValue::Str(pub_k_b64))
|
||||
.add_typed_default(
|
||||
|
|
@ -1259,18 +1430,26 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge)
|
||||
.with_id(cv.get_id())
|
||||
.with_request_id(cv)
|
||||
.with_receiver(sender_id);
|
||||
let _ = self.send_message(&res).await;
|
||||
}
|
||||
|
||||
async fn handle_app_challenge_response(self: Arc<Self>, cv: &CommunicationValue) {
|
||||
let sender_id = cv.get_sender();
|
||||
let sender_id = match cv.require_sender() {
|
||||
Ok(sender_id) => sender_id,
|
||||
Err(_) => {
|
||||
let _ = self
|
||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some((_, expected_challenge)) = self.app_challenges.remove(&sender_id) {
|
||||
if let Some(DataValue::Str(response)) = cv.get_data(DataType::Challenge) {
|
||||
if expected_challenge == *response {
|
||||
let res = CommunicationValue::new(CommunicationType::AppIdentificationResponse)
|
||||
.with_id(cv.get_id())
|
||||
.with_request_id(cv)
|
||||
.with_receiver(sender_id);
|
||||
let _ = self.send_message(&res).await;
|
||||
return;
|
||||
|
|
@ -1278,13 +1457,21 @@ impl OmikronConnection {
|
|||
}
|
||||
}
|
||||
let res = CommunicationValue::new(CommunicationType::ErrorInvalidChallenge)
|
||||
.with_id(cv.get_id())
|
||||
.with_request_id(cv)
|
||||
.with_receiver(sender_id);
|
||||
let _ = self.send_message(&res).await;
|
||||
}
|
||||
|
||||
async fn handle_save_app_data(self: Arc<Self>, cv: &CommunicationValue) {
|
||||
let sender_id = cv.get_sender();
|
||||
let sender_id = match cv.require_sender() {
|
||||
Ok(sender_id) => sender_id,
|
||||
Err(_) => {
|
||||
let _ = self
|
||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let app_data = cv
|
||||
.get_data(DataType::AppData)
|
||||
.as_str()
|
||||
|
|
@ -1297,13 +1484,21 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
let res = CommunicationValue::new(CommunicationType::SaveAppData)
|
||||
.with_id(cv.get_id())
|
||||
.with_request_id(cv)
|
||||
.with_receiver(sender_id);
|
||||
let _ = self.send_message(&res).await;
|
||||
}
|
||||
|
||||
async fn handle_load_app_data(self: Arc<Self>, cv: &CommunicationValue) {
|
||||
let sender_id = cv.get_sender();
|
||||
let sender_id = match cv.require_sender() {
|
||||
Ok(sender_id) => sender_id,
|
||||
Err(_) => {
|
||||
let _ = self
|
||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut app_data = String::new();
|
||||
|
||||
if let Some(session) = self.app_sessions.get(&sender_id) {
|
||||
|
|
@ -1312,7 +1507,7 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
let res = CommunicationValue::new(CommunicationType::LoadAppData)
|
||||
.with_id(cv.get_id())
|
||||
.with_request_id(cv)
|
||||
.with_receiver(sender_id)
|
||||
.add_typed_default(DataType::AppData, DataValue::Str(app_data));
|
||||
let _ = self.send_message(&res).await;
|
||||
|
|
@ -1353,9 +1548,9 @@ impl OmikronConnection {
|
|||
extra: Vec<(DataType, DataValue)>,
|
||||
) -> CommunicationValue {
|
||||
let mut message = CommunicationValue::new(ty)
|
||||
.with_id(request.get_id())
|
||||
.with_sender(mutation.sender_id as u64)
|
||||
.with_receiver(mutation.partner_id as u64)
|
||||
.with_request_id(request)
|
||||
.with_sender(wire_user_id(mutation.sender_id))
|
||||
.with_receiver(wire_user_id(mutation.partner_id))
|
||||
.add_typed_default(
|
||||
DataType::ChatPartnerId,
|
||||
DataValue::SignedNumber(mutation.sender_id as i128),
|
||||
|
|
@ -1371,18 +1566,26 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
async fn persist_and_deliver_remote_edit(&self, cv: &CommunicationValue) {
|
||||
let sender_id = match i64::try_from(cv.get_sender()) {
|
||||
Ok(sender_id) => sender_id,
|
||||
Err(_) => return,
|
||||
let sender_id = match cv
|
||||
.require_sender()
|
||||
.ok()
|
||||
.and_then(|id| i64::try_from(id).ok())
|
||||
{
|
||||
Some(sender_id) => sender_id,
|
||||
None => return,
|
||||
};
|
||||
let receiver_id = match i64::try_from(cv.get_receiver()) {
|
||||
Ok(receiver_id) if receiver_id > 0 => receiver_id,
|
||||
let receiver_id = match cv
|
||||
.require_receiver()
|
||||
.ok()
|
||||
.and_then(|id| i64::try_from(id).ok())
|
||||
{
|
||||
Some(receiver_id) if receiver_id > 0 => receiver_id,
|
||||
_ => return,
|
||||
};
|
||||
let Some(send_time) = data_i64(cv, DataType::SendTime).filter(|time| *time > 0) else {
|
||||
return;
|
||||
};
|
||||
let Some(content) = cv.get_data(DataType::Content).as_str() else {
|
||||
let Some(content) = cv.get_data(DataType::AppContent).as_str() else {
|
||||
return;
|
||||
};
|
||||
if chat_files::apply_remote_edit(receiver_id, sender_id, send_time, sender_id, content)
|
||||
|
|
@ -1393,12 +1596,20 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
async fn persist_and_deliver_remote_reaction(&self, cv: &CommunicationValue, add: bool) {
|
||||
let sender_id = match i64::try_from(cv.get_sender()) {
|
||||
Ok(sender_id) => sender_id,
|
||||
Err(_) => return,
|
||||
let sender_id = match cv
|
||||
.require_sender()
|
||||
.ok()
|
||||
.and_then(|id| i64::try_from(id).ok())
|
||||
{
|
||||
Some(sender_id) => sender_id,
|
||||
None => return,
|
||||
};
|
||||
let receiver_id = match i64::try_from(cv.get_receiver()) {
|
||||
Ok(receiver_id) if receiver_id > 0 => receiver_id,
|
||||
let receiver_id = match cv
|
||||
.require_receiver()
|
||||
.ok()
|
||||
.and_then(|id| i64::try_from(id).ok())
|
||||
{
|
||||
Some(receiver_id) if receiver_id > 0 => receiver_id,
|
||||
_ => return,
|
||||
};
|
||||
let Some(send_time) = data_i64(cv, DataType::SendTime).filter(|time| *time > 0) else {
|
||||
|
|
@ -1421,12 +1632,20 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
async fn persist_and_deliver_remote_delete(&self, cv: &CommunicationValue) {
|
||||
let sender_id = match i64::try_from(cv.get_sender()) {
|
||||
Ok(sender_id) => sender_id,
|
||||
Err(_) => return,
|
||||
let sender_id = match cv
|
||||
.require_sender()
|
||||
.ok()
|
||||
.and_then(|id| i64::try_from(id).ok())
|
||||
{
|
||||
Some(sender_id) => sender_id,
|
||||
None => return,
|
||||
};
|
||||
let receiver_id = match i64::try_from(cv.get_receiver()) {
|
||||
Ok(receiver_id) if receiver_id > 0 => receiver_id,
|
||||
let receiver_id = match cv
|
||||
.require_receiver()
|
||||
.ok()
|
||||
.and_then(|id| i64::try_from(id).ok())
|
||||
{
|
||||
Some(receiver_id) if receiver_id > 0 => receiver_id,
|
||||
_ => return,
|
||||
};
|
||||
let Some(send_time) = data_i64(cv, DataType::SendTime).filter(|time| *time > 0) else {
|
||||
|
|
@ -1449,7 +1668,7 @@ impl OmikronConnection {
|
|||
.await;
|
||||
return;
|
||||
};
|
||||
let Some(content) = cv.get_data(DataType::Content).as_str() else {
|
||||
let Some(content) = cv.get_data(DataType::AppContent).as_str() else {
|
||||
let _ = self
|
||||
.send_message(&error_response(cv, CommunicationType::ErrorInvalidData))
|
||||
.await;
|
||||
|
|
@ -1459,7 +1678,7 @@ impl OmikronConnection {
|
|||
CommunicationType::MessageEditLive,
|
||||
cv,
|
||||
&mutation,
|
||||
vec![(DataType::Content, DataValue::Str(content.to_string()))],
|
||||
vec![(DataType::AppContent, DataValue::Str(content.to_string()))],
|
||||
);
|
||||
if iota_storage::users::user_manager::get_user(mutation.partner_id).is_some()
|
||||
&& chat_files::apply_remote_edit(
|
||||
|
|
@ -1558,9 +1777,13 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
async fn handle_message_delete_live(self: Arc<Self>, cv: &CommunicationValue) {
|
||||
let sender_id = match i64::try_from(cv.get_sender()) {
|
||||
Ok(sender_id) => sender_id,
|
||||
Err(_) => return,
|
||||
let sender_id = match cv
|
||||
.require_sender()
|
||||
.ok()
|
||||
.and_then(|id| i64::try_from(id).ok())
|
||||
{
|
||||
Some(sender_id) => sender_id,
|
||||
None => return,
|
||||
};
|
||||
if iota_storage::users::user_manager::get_user(sender_id).is_none() {
|
||||
self.persist_and_deliver_remote_delete(cv).await;
|
||||
|
|
@ -1741,7 +1964,9 @@ impl OmikronConnection {
|
|||
timeout_duration: Option<Duration>,
|
||||
) -> Result<CommunicationValue, String> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let msg_id = cv.get_id();
|
||||
let msg_id = cv
|
||||
.require_id()
|
||||
.map_err(|error| format!("cannot await response without a message id: {error}"))?;
|
||||
|
||||
WAITING_TASKS.insert(
|
||||
msg_id,
|
||||
|
|
@ -1858,6 +2083,8 @@ impl OmikronConnection {
|
|||
/// 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");
|
||||
let identity_secret =
|
||||
load_identity_secret().map_err(|error| OmikronError::Internal(error.to_string()))?;
|
||||
self.stop().await;
|
||||
|
||||
let path = identity_path();
|
||||
|
|
@ -1877,7 +2104,10 @@ impl OmikronConnection {
|
|||
}
|
||||
|
||||
let keyring = crypto_helper::generate_keyring();
|
||||
if let Some(parent) = path.parent() {
|
||||
if let Some(parent) = path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
{
|
||||
std::fs::create_dir_all(parent).map_err(|error| {
|
||||
OmikronError::Internal(format!(
|
||||
"could not create identity directory {}: {error}",
|
||||
|
|
@ -1885,7 +2115,7 @@ impl OmikronConnection {
|
|||
))
|
||||
})?;
|
||||
}
|
||||
mtp::files::save_keyring_raw(&keyring, path).map_err(|error| {
|
||||
save_protected_keyring_verified(&keyring, path, &identity_secret).map_err(|error| {
|
||||
OmikronError::Internal(format!(
|
||||
"could not save new identity {}: {error}",
|
||||
path.display()
|
||||
|
|
@ -2052,3 +2282,80 @@ impl OmikronClient for OmikronConnection {
|
|||
Self::is_connected(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod identity_tests {
|
||||
use super::*;
|
||||
|
||||
fn test_path(name: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
"iota-identity-{name}-{}-{}",
|
||||
std::process::id(),
|
||||
Uuid::new_v4()
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_identity_is_protected_and_survives_reload() {
|
||||
let path = test_path("reload");
|
||||
let passphrase = b"test identity secret";
|
||||
let keyring = load_or_migrate_keyring_at(&path, None, passphrase).expect("identity saves");
|
||||
let reloaded = load_or_migrate_keyring_at(&path, None, passphrase).expect("identity loads");
|
||||
assert_eq!(
|
||||
keyring.try_to_bytes().expect("keyring serializes"),
|
||||
reloaded.try_to_bytes().expect("keyring serializes")
|
||||
);
|
||||
assert!(mtp::files::load_keyring(&path, b"wrong secret").is_err());
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_existing_identity_does_not_generate_a_replacement() {
|
||||
let path = test_path("corrupt");
|
||||
fs::write(&path, b"not a keyring").expect("corrupt fixture writes");
|
||||
let error = load_or_migrate_keyring_at(&path, None, b"test identity secret")
|
||||
.expect_err("corrupt identity must fail");
|
||||
assert!(matches!(error, IdentityError::Storage(_)));
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_raw_identity_is_migrated_only_when_the_raw_format_is_valid() {
|
||||
let path = test_path("legacy");
|
||||
let keyring = crypto_helper::generate_keyring();
|
||||
let mut raw = b"MTMK".to_vec();
|
||||
raw.push(1);
|
||||
raw.extend_from_slice(&keyring.try_to_bytes().expect("keyring serializes"));
|
||||
fs::write(&path, raw).expect("legacy fixture writes");
|
||||
|
||||
let migrated = load_or_migrate_keyring_at(&path, None, b"test identity secret")
|
||||
.expect("legacy identity migrates");
|
||||
assert_eq!(
|
||||
migrated.try_to_bytes().expect("keyring serializes"),
|
||||
keyring.try_to_bytes().expect("keyring serializes")
|
||||
);
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_directory_failure_is_returned() {
|
||||
let parent = test_path("parent-file");
|
||||
fs::write(&parent, b"not a directory").expect("parent fixture writes");
|
||||
let path = parent.join("iota.mk");
|
||||
let error = load_or_migrate_keyring_at(&path, None, b"test identity secret")
|
||||
.expect_err("directory failure must be returned");
|
||||
assert!(matches!(error, IdentityError::Directory(_)));
|
||||
let _ = fs::remove_file(parent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconnect_jitter_stays_bounded_by_the_exponential_delay_ceiling() {
|
||||
for _ in 0..32 {
|
||||
let delay = jittered_reconnect_delay(Duration::from_secs(5));
|
||||
assert!(delay >= Duration::from_secs(4));
|
||||
assert!(delay <= Duration::from_secs(6));
|
||||
}
|
||||
|
||||
assert!(jittered_reconnect_delay(Duration::from_secs(600)) <= MAX_RECONNECT_DELAY);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ mod tests {
|
|||
use super::{CreateUserError, request_user_id, valid_username};
|
||||
use crate::{OmikronClient, OmikronError};
|
||||
use async_trait::async_trait;
|
||||
use iota_util::mtp_compat::CommunicationValueCompat;
|
||||
use iota_connection::message_common::CommunicationResponseExt;
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
@ -397,7 +397,7 @@ mod tests {
|
|||
_: Duration,
|
||||
) -> Result<CommunicationValue, OmikronError> {
|
||||
assert!(request.is_type(CommunicationType::GetRegister));
|
||||
Ok(self.response.clone().with_id(request.get_id()))
|
||||
Ok(self.response.clone().with_request_id(request))
|
||||
}
|
||||
|
||||
async fn reconnect(&self) -> Result<(), OmikronError> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue