[WIP] 0.3.0 mtp update

This commit is contained in:
Alex Emmet 2026-08-18 22:37:55 +02:00
commit dfe8e6efa7
No known key found for this signature in database
15 changed files with 1038 additions and 595 deletions

View file

@ -8,7 +8,9 @@ use uuid::Uuid;
use crate::anonymous_clients::anonymous_manager::{self, generate_username};
use crate::app_state::AppState;
use crate::calls::call_group::call_invite_secret_from_cv;
use crate::rho::connection::{GeneralConnection, MtpReceiver, MtpSender};
use crate::rho::connection::{
GeneralConnection, MtpReceiver, MtpSender, MtpValueCompat, OptionalDataValueCompat,
};
use crate::util::data_type_id;
use crate::util::logger::PrintType;
use crate::{log_cv_in, log_cv_out, log_out};
@ -108,6 +110,12 @@ impl AnonymousClientConnection {
let _permit = permit;
log_cv_in!(PrintType::Client, &cv);
if cv.is_type(CommunicationType::Relay) {
self.send_error_response(&cv.get_id(), CommunicationType::ErrorNotAuthenticated)
.await;
return;
}
if cv.is_type(CommunicationType::Identification) {
let call_id = Uuid::parse_str(cv.get_data(DataType::CallId).as_str().unwrap_or(""))
.unwrap_or(Uuid::new_v4());
@ -153,19 +161,31 @@ impl AnonymousClientConnection {
let mut json_invitee = Vec::new();
let _ = json_invitee.push((
DataType::UserId,
call_invitee_cv.get_data(DataType::UserId).clone(),
call_invitee_cv
.get_data(DataType::UserId)
.cloned()
.unwrap_or(DataValue::Null),
));
let _ = json_invitee.push((
DataType::Username,
call_invitee_cv.get_data(DataType::Username).clone(),
call_invitee_cv
.get_data(DataType::Username)
.cloned()
.unwrap_or(DataValue::Null),
));
let _ = json_invitee.push((
DataType::Display,
call_invitee_cv.get_data(DataType::Display).clone(),
call_invitee_cv
.get_data(DataType::Display)
.cloned()
.unwrap_or(DataValue::Null),
));
let _ = json_invitee.push((
DataType::Avatar,
call_invitee_cv.get_data(DataType::Avatar).clone(),
call_invitee_cv
.get_data(DataType::Avatar)
.cloned()
.unwrap_or(DataValue::Null),
));
let _ = invited.push(DataValue::Container(
@ -223,9 +243,7 @@ impl AnonymousClientConnection {
// Presence is account-scoped and anonymous sessions have no
// persisted account preference to change.
if cv.is_type(CommunicationType::SetUserState)
|| cv.is_type(CommunicationType::ClientChanged)
{
if cv.is_type(CommunicationType::ClientChanged) {
self.send_error_response(&cv.get_id(), CommunicationType::ErrorNoUserId)
.await;
return;
@ -333,7 +351,7 @@ impl AnonymousClientConnection {
}
let call_id = match cv.get_data(DataType::CallId) {
DataValue::Str(id_str) => match Uuid::parse_str(&id_str.to_string()) {
Some(DataValue::Str(id_str)) => match Uuid::parse_str(id_str) {
Ok(id) => id,
Err(_) => {
self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidCallId)
@ -443,7 +461,7 @@ impl AnonymousClientConnection {
let user_id = self.get_user_id();
let call_id = match cv.get_data(DataType::CallId) {
DataValue::Str(id_str) => match Uuid::parse_str(&id_str.to_string()) {
Some(DataValue::Str(id_str)) => match Uuid::parse_str(id_str) {
Ok(id) => id,
Err(_) => {
self.send_error_response(&cv.get_id(), CommunicationType::ErrorInvalidCallId)

View file

@ -8,6 +8,7 @@ use uuid::Uuid;
use crate::{
calls::{call_util::LiveKitService, caller::Caller, error::CallError},
omega::omega_connection::OmegaConnection,
rho::connection::OptionalDataValueCompat,
util::data_type_id,
};
@ -95,7 +96,7 @@ impl CallSecretEnvelope {
}
pub fn call_invite_secret_from_cv(cv: &CommunicationValue) -> Option<CallSecretEnvelope> {
CallSecretEnvelope::from_data_value(cv.get_data(DataType::CallSecret))
CallSecretEnvelope::from_data_value(cv.get_data(DataType::CallSecret)?)
}
impl CallGroup {
@ -348,8 +349,11 @@ mod tests {
receiver_secret.clone().to_data_value(),
);
let Some(call_secret) = cv.get_data(DataType::CallSecret) else {
panic!("call invite did not contain a call secret");
};
assert_eq!(
CallSecretEnvelope::from_data_value(cv.get_data(DataType::CallSecret)),
CallSecretEnvelope::from_data_value(call_secret),
Some(receiver_secret)
);
}

View file

@ -2,6 +2,8 @@ use super::capabilities::PeerCapabilities;
use crate::{
config::Config,
log_cv_in, log_cv_out, log_err, log_in, log_out,
rho::connection::{MtpValueCompat, OptionalDataValueCompat},
rho::relay_router,
rho::rho_manager::RhoManager,
util::{data_type_id, logger::PrintType},
};
@ -597,6 +599,37 @@ impl OmegaConnection {
continue;
}
if cv.is_type(CommunicationType::Relay) {
let destination_iota = cv.receiver().unwrap_or_default();
let request_id = cv.get_id();
let response = match relay_router::route_from_omega(&self.rho, cv).await {
Ok(()) => CommunicationValue::new(CommunicationType::Success)
.with_id(request_id),
Err(error) => {
log_err!(
self.omikron_id as i64,
PrintType::Omega,
"Relay routing failed for destination Iota {}: {}",
destination_iota,
error
);
CommunicationValue::new(
relay_router::error_response_type(&error),
)
.with_id(request_id)
}
};
if let Err(send_error) = self.send_message_result(&response).await {
log_err!(
self.omikron_id as i64,
PrintType::Omega,
"Relay routing response failed: {}",
send_error
);
}
continue;
}
let msg_id = cv.get_id();
if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) {
if (task.task)(self.clone(), cv.clone()) {
@ -605,7 +638,7 @@ impl OmegaConnection {
}
if cv.is_type(CommunicationType::IotaUserData) {
if let DataValue::Array(users) = cv.get_data(DataType::UserIds) {
if let Some(DataValue::Array(users)) = cv.get_data(DataType::UserIds) {
let mut user_ids = Vec::new();
for value in users {
if let DataValue::SignedNumber(user_id) = value {

View file

@ -1,9 +1,12 @@
use crate::anonymous_clients::anonymous_manager;
use crate::app_state::AppState;
use crate::rho::connection::{GeneralConnection, MtpReceiver, MtpSender};
use crate::rho::connection::{
GeneralConnection, MtpReceiver, MtpSender, MtpValueCompat, OptionalDataValueCompat,
};
use crate::rho::relay_router::{self, RelaySource};
use crate::rho::rho_connection::RhoConnection;
use crate::util::logger::PrintType;
use crate::{log_cv_in, log_cv_out, log_err, log_in, log_out};
use crate::{log_cv_in, log_cv_out, log_err, log_out};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::sync::Arc;
use std::time::Duration;
@ -88,6 +91,59 @@ impl AppConnection {
tokio::spawn(async move {
log_cv_in!(PrintType::App, cv);
if cv.is_type(CommunicationType::Relay) {
let cv = relay_router::ensure_relay_frame_id(cv);
let request_id = cv.get_id();
let next_hop = cv.receiver().unwrap_or_default();
let result = match self.get_rho_connection().await {
Some(rho) => {
relay_router::route_relay(
&self.state,
RelaySource::Client {
iota_id: rho.get_iota_id().await,
},
cv,
)
.await
}
None => Err(relay_router::RelayRouteError::DestinationIotaNotLocal),
};
let response = match result {
Ok(()) => {
CommunicationValue::new(CommunicationType::Success).with_id(request_id)
}
Err(error) => {
log_err!(
self.user_id as i64,
PrintType::App,
"Relay routing failed for destination Iota {}: {}",
next_hop,
error
);
CommunicationValue::new(relay_router::error_response_type(&error))
.with_id(request_id)
}
};
self.send_message(&response).await;
return;
}
if cv.is_type(CommunicationType::Success) {
if let Some(rho) = self.get_rho_connection().await {
rho.forward_relay_ack(self.user_id, cv.get_id()).await;
}
return;
}
if matches!(
relay_router::message_security_class(&cv),
relay_router::MessageSecurityClass::RelayOnly
) {
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidData)
.await;
return;
}
if cv.is_type(CommunicationType::GetUserData) {
if let Some(anonymous) = {
if let Some(user_id) = cv.get_data(DataType::UserId).as_number() {
@ -135,8 +191,14 @@ impl AppConnection {
.await;
return;
}
// Forward other messages to Iota
self.forward_to_iota(cv).await;
log_err!(
self.user_id as i64,
PrintType::App,
"Rejected unsupported communication type {}",
cv.get_type()
);
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidData)
.await;
});
}
async fn handle_omega_forward(self: Arc<Self>, cv: CommunicationValue) {
@ -154,123 +216,6 @@ impl AppConnection {
});
}
/// Forward message to Iota
async fn forward_to_iota(self: Arc<Self>, cv: CommunicationValue) {
let sender_user_id = self.get_user_id().await;
let msg_id = cv.get_id();
let msg_type = cv.get_type();
log_in!(
sender_user_id as i64,
PrintType::App,
"Forwarding app->iota: sender={} type={:?} id={} receiver={}",
sender_user_id,
msg_type,
msg_id,
cv.get_receiver()
);
if cv.is_type(CommunicationType::AddConversation)
&& cv.get_data(DataType::ChatPartnerId).as_number().is_none()
{
let chat_partner_name = cv
.get_data(DataType::ChatPartnerName)
.as_str()
.unwrap_or("")
.to_string();
if anonymous_manager::get_anonymous_user_by_name(chat_partner_name.to_string())
.await
.is_some()
{
self.send_error_response(cv.get_id(), CommunicationType::ErrorAnonymous)
.await;
return;
}
let load_uuid_response = self
.state
.omega
.clone()
.await_response(
&CommunicationValue::new(CommunicationType::GetUserData)
.with_id(cv.clone().get_id())
.add_typed_default(
DataType::Username,
DataValue::Str(chat_partner_name.clone()),
),
Some(Duration::from_secs(20)),
)
.await;
let chat_partner_id = {
if let Ok(load_uuid_response) = load_uuid_response {
load_uuid_response.get_data(DataType::UserId).clone()
} else {
DataValue::Null
}
};
if let Some(rho_conn) = self.get_rho_connection().await {
let iota_id = rho_conn.get_iota_id().await;
log_in!(
sender_user_id as i64,
PrintType::App,
"Resolved rho for add_conversation: sender={} -> iota_id={} id={}",
sender_user_id,
iota_id,
msg_id
);
let updated_cv = cv
.with_sender(sender_user_id as u64)
.add_typed_default(DataType::ChatPartnerId, chat_partner_id);
rho_conn.message_to_iota(updated_cv).await;
} else {
log_err!(
sender_user_id as i64,
PrintType::App,
"No rho/iota mapping found for add_conversation sender={} type={:?} id={}",
sender_user_id,
msg_type,
msg_id
);
}
return;
}
if let Some(rho_conn) = self.get_rho_connection().await {
let iota_id = rho_conn.get_iota_id().await;
log_in!(
sender_user_id as i64,
PrintType::App,
"Resolved rho for forward: sender={} -> iota_id={} type={:?} id={}",
sender_user_id,
iota_id,
msg_type,
msg_id
);
let updated_cv = cv.with_sender(sender_user_id as u64);
rho_conn.message_to_iota(updated_cv).await;
} else {
log_err!(
sender_user_id as i64,
PrintType::App,
"No rho/iota mapping found for sender={} type={:?} id={}",
sender_user_id,
msg_type,
msg_id
);
let error_cv = CommunicationValue::new(CommunicationType::ErrorNoIota)
.with_id(msg_id)
.add_typed_default(
DataType::UserId,
DataValue::SignedNumber(sender_user_id.into()),
);
self.send_message(&error_cv).await;
}
}
/// Send error response
async fn send_error_response(self: Arc<Self>, message_id: u32, error_type: CommunicationType) {
let error = CommunicationValue::new(error_type).with_id(message_id);

View file

@ -2,10 +2,13 @@ use crate::anonymous_clients::anonymous_manager;
use crate::app_state::AppState;
use crate::calls::call_group::call_invite_secret_from_cv;
use crate::data::user::UserStatus;
use crate::rho::connection::{GeneralConnection, MtpReceiver, MtpSender};
use crate::rho::connection::{
GeneralConnection, MtpReceiver, MtpSender, MtpValueCompat, OptionalDataValueCompat,
};
use crate::rho::relay_router::{self, RelaySource};
use crate::rho::rho_connection::RhoConnection;
use crate::util::logger::PrintType;
use crate::{log_cv_in, log_cv_out, log_err, log_in, log_out};
use crate::{log_cv_in, log_cv_out, log_err, log_out};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::str::FromStr;
use std::sync::Arc;
@ -97,6 +100,16 @@ impl ClientConnection {
let _ = self.sender.send(&cv).await;
}
pub async fn send_relay(&self, cv: &CommunicationValue) -> Result<(), String> {
if !*self.is_open.read().await {
return Err("client connection is closed".to_string());
}
self.sender
.send(cv)
.await
.map_err(|error| error.to_string())
}
/// Handle incoming message from client
pub async fn handle_message(self: Arc<Self>, cv: CommunicationValue) {
let Ok(permit) = self.message_slots.clone().acquire_owned().await else {
@ -108,8 +121,56 @@ impl ClientConnection {
let mut cv = cv;
if cv.is_type(CommunicationType::SetUserState) {
self.handle_set_user_state(cv).await;
if cv.is_type(CommunicationType::Relay) {
cv = relay_router::ensure_relay_frame_id(cv);
let request_id = cv.get_id();
let next_hop = cv.receiver().unwrap_or_default();
let result = match self.get_rho_connection().await {
Some(rho) => {
relay_router::route_relay(
&self.state,
RelaySource::Client {
iota_id: rho.get_iota_id().await,
},
cv,
)
.await
}
None => Err(relay_router::RelayRouteError::DestinationIotaNotLocal),
};
let response = match result {
Ok(()) => {
CommunicationValue::new(CommunicationType::Success).with_id(request_id)
}
Err(error) => {
log_err!(
self.user_id as i64,
PrintType::Client,
"Relay routing failed for destination Iota {}: {}",
next_hop,
error
);
CommunicationValue::new(relay_router::error_response_type(&error))
.with_id(request_id)
}
};
self.send_message(&response).await;
return;
}
if cv.is_type(CommunicationType::Success) {
if let Some(rho) = self.get_rho_connection().await {
rho.forward_relay_ack(self.user_id, cv.get_id()).await;
}
return;
}
if matches!(
relay_router::message_security_class(&cv),
relay_router::MessageSecurityClass::RelayOnly
) {
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidData)
.await;
return;
}
@ -120,11 +181,13 @@ impl ClientConnection {
&& cv.get_data_opt(DataType::UserState).is_some()
{
self.handle_set_user_state(
CommunicationValue::new(CommunicationType::SetUserState)
CommunicationValue::new(CommunicationType::ClientChanged)
.with_id(cv.get_id())
.add_typed_default(
DataType::UserState,
cv.get_data(DataType::UserState).clone(),
cv.get_data(DataType::UserState)
.cloned()
.unwrap_or(DataValue::Null),
),
)
.await;
@ -216,10 +279,12 @@ impl ClientConnection {
&& cv.get_data_opt(DataType::OnlineStatus).is_some()
{
let mut profile_request = cv.clone();
let preference = profile_request.remove_data(DataType::OnlineStatus);
let state_request = CommunicationValue::new(CommunicationType::SetUserState)
let preference = profile_request
.remove_data(DataType::OnlineStatus)
.unwrap_or(DataValue::Null);
let state_request = CommunicationValue::new(CommunicationType::ClientChanged)
.with_id(cv.get_id())
.add_typed_default(DataType::UserState, preference.unwrap());
.add_typed_default(DataType::UserState, preference);
let state_response = match self.request_set_user_state(state_request).await {
Ok(response) => response,
Err(error_type) => {
@ -346,8 +411,14 @@ impl ClientConnection {
);
}
// Forward other messages to Iota
self.forward_to_iota(cv).await;
log_err!(
self.user_id as i64,
PrintType::Client,
"Rejected unsupported communication type {}",
cv.get_type()
);
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidData)
.await;
});
}
async fn handle_omega_forward(self: Arc<Self>, cv: CommunicationValue) {
@ -384,52 +455,25 @@ impl ClientConnection {
else {
return Err(CommunicationType::ErrorInvalidData);
};
let Some(rho) = self.get_rho_connection().await else {
let Some(_rho) = self.get_rho_connection().await else {
return Err(CommunicationType::ErrorNoIota);
};
let request = if self.state.omega.supports_set_user_state().await {
CommunicationValue::new(CommunicationType::SetUserState)
.with_id(cv.get_id())
.with_sender(self.user_id)
.add_typed_default(
DataType::UserId,
DataValue::SignedNumber(self.user_id as i128),
)
.add_typed_default(
DataType::SessionId,
DataValue::SignedNumber(self.session_id as i128),
)
.add_typed_default(
DataType::IotaId,
DataValue::SignedNumber(rho.get_iota_id().await as i128),
)
.add_typed_default(DataType::UserState, DataValue::Str(state.to_string()))
} else {
// Legacy Omega accepts the original ClientChanged request. It is
// intentionally limited to the authenticated user's ID and a
// writable preference, with no derived connectivity state.
CommunicationValue::new(CommunicationType::ClientChanged)
.with_id(cv.get_id())
.with_sender(self.user_id)
.add_typed_default(
DataType::UserId,
DataValue::SignedNumber(self.user_id as i128),
)
.add_typed_default(DataType::UserState, DataValue::Str(state.to_string()))
};
if !self.state.omega.supports_set_user_state().await {
self.state
.omega
.send_message_result(&request)
.await
.map_err(|_| CommunicationType::ErrorInternal)?;
return Ok(CommunicationValue::new(CommunicationType::Success)
.with_id(cv.get_id())
.add_typed_default(DataType::UserState, DataValue::Str(state.to_string())));
}
self.await_omega_response(request)
let request = CommunicationValue::new(CommunicationType::ClientChanged)
.with_id(cv.get_id())
.with_sender(self.user_id)
.add_typed_default(
DataType::UserId,
DataValue::SignedNumber(self.user_id as i128),
)
.add_typed_default(DataType::UserState, DataValue::Str(state.to_string()));
self.state
.omega
.send_message_result(&request)
.await
.map_err(|_| CommunicationType::ErrorInternal)
.map_err(|_| CommunicationType::ErrorInternal)?;
return Ok(CommunicationValue::new(CommunicationType::Success)
.with_id(cv.get_id())
.add_typed_default(DataType::UserState, DataValue::Str(state.to_string())));
}
async fn handle_set_user_state(self: Arc<Self>, cv: CommunicationValue) {
@ -449,7 +493,7 @@ impl ClientConnection {
}
let call_id = match cv.get_data(DataType::CallId) {
DataValue::Str(id_str) => match Uuid::parse_str(id_str.as_str()) {
Some(DataValue::Str(id_str)) => match Uuid::parse_str(id_str) {
Ok(id) => id,
Err(_) => {
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidCallId)
@ -559,7 +603,7 @@ impl ClientConnection {
let user_id = self.get_user_id().await;
let call_id = match cv.get_data(DataType::CallId) {
DataValue::Str(id_str) => match Uuid::parse_str(id_str.as_str()) {
Some(DataValue::Str(id_str)) => match Uuid::parse_str(id_str) {
Ok(id) => id,
Err(_) => {
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidCallId)
@ -600,7 +644,7 @@ impl ClientConnection {
let user_id = self.get_user_id().await;
let call_id = match cv.get_data(DataType::CallId) {
DataValue::Str(id_str) => match Uuid::parse_str(id_str.as_str()) {
Some(DataValue::Str(id_str)) => match Uuid::parse_str(id_str) {
Ok(id) => id,
Err(_) => {
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidCallId)
@ -750,7 +794,10 @@ impl ClientConnection {
let resolver = match TokioAsyncResolver::tokio_from_system_conf() {
Ok(r) => r,
Err(_) => {
let path_data = cv.get_data(DataType::Path).clone();
let path_data = cv
.get_data(DataType::Path)
.cloned()
.unwrap_or(DataValue::Null);
let error_cv = CommunicationValue::new(CommunicationType::ErrorNotFound)
.with_id(cv.get_id())
.add_typed_default(DataType::Path, path_data);
@ -786,14 +833,20 @@ impl ClientConnection {
return;
}
let path_data = cv.get_data(DataType::Path).clone();
let path_data = cv
.get_data(DataType::Path)
.cloned()
.unwrap_or(DataValue::Null);
let error_cv = CommunicationValue::new(CommunicationType::ErrorNotFound)
.with_id(cv.get_id())
.add_typed_default(DataType::Path, path_data);
self.send_message(&error_cv).await;
}
Err(_) => {
let path_data = cv.get_data(DataType::Path).clone();
let path_data = cv
.get_data(DataType::Path)
.cloned()
.unwrap_or(DataValue::Null);
let error_cv = CommunicationValue::new(CommunicationType::ErrorNotFound)
.with_id(cv.get_id())
.add_typed_default(DataType::Path, path_data);
@ -804,133 +857,16 @@ impl ClientConnection {
return;
}
let path_data = cv.get_data(DataType::Path).clone();
let path_data = cv
.get_data(DataType::Path)
.cloned()
.unwrap_or(DataValue::Null);
let error_cv = CommunicationValue::new(CommunicationType::ErrorNotFound)
.with_id(cv.get_id())
.add_typed_default(DataType::Path, path_data);
self.send_message(&error_cv).await;
}
/// Forward message to Iota
async fn forward_to_iota(self: Arc<Self>, cv: CommunicationValue) {
let sender_user_id = self.get_user_id().await;
let msg_id = cv.get_id();
let msg_type = cv.get_type();
log_in!(
sender_user_id as i64,
PrintType::Client,
"Forwarding client->iota: sender={} type={:?} id={} receiver={}",
sender_user_id,
msg_type,
msg_id,
cv.get_receiver()
);
if cv.is_type(CommunicationType::AddConversation)
&& cv
.get_data(DataType::ChatPartnerId)
.as_signed_number()
.is_none()
{
let chat_partner_name = cv
.get_data(DataType::ChatPartnerName)
.as_str()
.unwrap_or("")
.to_string();
if anonymous_manager::get_anonymous_user_by_name(chat_partner_name.to_string())
.await
.is_some()
{
self.send_error_response(cv.get_id(), CommunicationType::ErrorAnonymous)
.await;
return;
}
let load_uuid_response = self
.state
.omega
.clone()
.await_response(
&CommunicationValue::new(CommunicationType::GetUserData)
.with_id(cv.clone().get_id())
.add_typed_default(
DataType::Username,
DataValue::Str(chat_partner_name.clone()),
),
Some(Duration::from_secs(20)),
)
.await;
let chat_partner_id = {
if let Ok(load_uuid_response) = load_uuid_response {
load_uuid_response.get_data(DataType::UserId).clone()
} else {
DataValue::Null
}
};
if let Some(rho_conn) = self.get_rho_connection().await {
let iota_id = rho_conn.get_iota_id().await;
log_in!(
sender_user_id as i64,
PrintType::Client,
"Resolved rho for add_conversation: sender={} -> iota_id={} id={}",
sender_user_id,
iota_id,
msg_id
);
let updated_cv = cv
.with_sender(sender_user_id as u64)
.add_typed_default(DataType::ChatPartnerId, chat_partner_id);
rho_conn.message_to_iota(updated_cv).await;
} else {
log_err!(
sender_user_id as i64,
PrintType::Client,
"No rho/iota mapping found for add_conversation sender={} type={:?} id={}",
sender_user_id,
msg_type,
msg_id
);
}
return;
}
if let Some(rho_conn) = self.get_rho_connection().await {
let iota_id = rho_conn.get_iota_id().await;
log_in!(
sender_user_id as i64,
PrintType::Client,
"Resolved rho for forward: sender={} -> iota_id={} type={:?} id={}",
sender_user_id,
iota_id,
msg_type,
msg_id
);
let updated_cv = cv.with_sender(sender_user_id as u64);
rho_conn.message_to_iota(updated_cv).await;
} else {
log_err!(
sender_user_id as i64,
PrintType::Client,
"No rho/iota mapping found for sender={} type={:?} id={}",
sender_user_id,
msg_type,
msg_id
);
let error_cv = CommunicationValue::new(CommunicationType::ErrorNoIota)
.with_id(msg_id)
.add_typed_default(
DataType::UserId,
DataValue::SignedNumber(sender_user_id.into()),
);
self.send_message(&error_cv).await;
}
}
/// Send error response
async fn send_error_response(self: Arc<Self>, message_id: u32, error_type: CommunicationType) {
let error = CommunicationValue::new(error_type).with_id(message_id);

View file

@ -20,6 +20,66 @@ use mtp::webserver::{WebMTPConnection, WebMtpReceiver, WebMtpSender};
pub type MtpSender = WebMtpSender;
pub type MtpReceiver = WebMtpReceiver;
/*
* MTP 0.3 exposes absent frame fields and data entries as Options. These
* adapters keep legacy control handlers explicit while Relay code uses the
* native optional accessors directly.
*/
pub(crate) trait MtpValueCompat {
fn get_id(&self) -> u32;
fn get_sender(&self) -> u64;
fn get_receiver(&self) -> u64;
fn get_data_opt(&self, data_type: DataType) -> Option<&DataValue>;
}
impl MtpValueCompat for CommunicationValue {
fn get_id(&self) -> u32 {
self.id().unwrap_or_default()
}
fn get_sender(&self) -> u64 {
self.sender().unwrap_or_default()
}
fn get_receiver(&self) -> u64 {
self.receiver().unwrap_or_default()
}
fn get_data_opt(&self, data_type: DataType) -> Option<&DataValue> {
self.get_data(data_type)
}
}
pub(crate) trait OptionalDataValueCompat {
fn as_bool(&self) -> Option<bool>;
fn as_number(&self) -> Option<i128>;
fn as_signed_number(&self) -> Option<i128>;
fn as_str(&self) -> Option<&str>;
fn as_bytes(&self) -> Option<Vec<u8>>;
}
impl OptionalDataValueCompat for Option<&DataValue> {
fn as_bool(&self) -> Option<bool> {
self.and_then(|value| value.as_bool())
}
fn as_number(&self) -> Option<i128> {
self.and_then(|value| value.as_number())
}
fn as_signed_number(&self) -> Option<i128> {
self.and_then(|value| value.as_signed_number())
}
fn as_str(&self) -> Option<&str> {
self.and_then(|value| value.as_str())
}
fn as_bytes(&self) -> Option<Vec<u8>> {
self.and_then(|value| value.as_bytes())
}
}
/*
* How a connection identified itself during the mtp handshake driven by
* `server.rs` ("iota" / "client" authenticated logins, "anonymous"
@ -194,7 +254,7 @@ impl GeneralConnection {
return None;
};
let DataValue::SignedNumber(iota_id) = user_data_cv.get_data(DataType::IotaId) else {
let Some(DataValue::SignedNumber(iota_id)) = user_data_cv.get_data(DataType::IotaId) else {
return None;
};
let rho = self
@ -264,7 +324,7 @@ impl GeneralConnection {
else {
return;
};
let DataValue::Array(users) = response.get_data(DataType::UserIds) else {
let Some(DataValue::Array(users)) = response.get_data(DataType::UserIds) else {
return;
};
let user_ids = users

View file

@ -5,7 +5,10 @@ use crate::log_cv_out;
use crate::log_err;
use crate::log_in;
use crate::log_out;
use crate::rho::connection::{GeneralConnection, MtpReceiver, MtpSender};
use crate::rho::connection::{
GeneralConnection, MtpReceiver, MtpSender, MtpValueCompat, OptionalDataValueCompat,
};
use crate::rho::relay_router::{self, RelaySource};
use crate::util::data_type_id;
use crate::util::logger::PrintType;
use dashmap::DashMap;
@ -38,7 +41,7 @@ fn contact_snapshot(value: &CommunicationValue) -> Option<(i64, i64, Vec<i64>)>
.as_signed_number()
.and_then(|id| i64::try_from(id).ok())
.filter(|id| *id > 0)?;
let DataValue::Array(values) = value.get_data(DataType::UserIds) else {
let Some(DataValue::Array(values)) = value.get_data(DataType::UserIds) else {
return None;
};
@ -76,7 +79,6 @@ pub struct IotaConnection {
pub waiting_tasks:
DashMap<u32, Box<dyn Fn(Arc<IotaConnection>, CommunicationValue) -> bool + Send + Sync>>,
pub rho_connection: Arc<RwLock<Option<Arc<RhoConnection>>>>,
pending_chat_secrets: DashMap<u64, Vec<CommunicationValue>>,
message_slots: Arc<tokio::sync::Semaphore>,
}
@ -92,7 +94,6 @@ impl IotaConnection {
iota_id: iota_id,
client_version: general.client_version.read().await.clone(),
waiting_tasks: DashMap::new(),
pending_chat_secrets: DashMap::new(),
message_slots: Arc::new(tokio::sync::Semaphore::new(32)),
})
}
@ -150,10 +151,6 @@ impl IotaConnection {
let user_ids_i64: Vec<i64> = user_ids.into_iter().map(|u| u as i64).collect();
rho_conn.set_user_ids(user_ids_i64).await;
}
for user_id in self.get_user_ids().await {
self.flush_pending_chat_secrets(user_id).await;
}
}
pub async fn add_user_id(&self, user_id: u64) {
@ -170,33 +167,9 @@ impl IotaConnection {
if let Some(rho_conn) = self.get_rho_connection().await {
rho_conn.add_user_id(user_id as i64).await;
}
self.flush_pending_chat_secrets(user_id).await;
}
}
async fn flush_pending_chat_secrets(&self, user_id: u64) {
let Some((_, messages)) = self.pending_chat_secrets.remove(&user_id) else {
return;
};
for message in messages {
self.send_message(&message).await;
}
}
fn store_pending_chat_secret(&self, cv: CommunicationValue) {
let receiver_id = cv.get_receiver();
if receiver_id == 0 || !cv.is_type(CommunicationType::SetChatSecret) {
return;
}
self.pending_chat_secrets
.entry(receiver_id)
.or_default()
.push(cv);
}
/// Set the RhoConnection reference
pub async fn set_rho_connection(&self, rho_connection: Arc<RhoConnection>) {
let mut rho_ref = self.rho_connection.write().await;
@ -249,12 +222,58 @@ impl IotaConnection {
}
}
pub async fn send_relay(&self, cv: &CommunicationValue) -> Result<(), String> {
self.sender
.send(cv)
.await
.map_err(|error| error.to_string())
}
/// Handle incoming message from Iota
pub async fn handle_message(self: Arc<Self>, cv: CommunicationValue) {
let Ok(permit) = self.message_slots.clone().acquire_owned().await else {
return;
};
let _permit = permit;
if cv.is_type(CommunicationType::Relay) {
let cv = relay_router::ensure_relay_frame_id(cv);
let request_id = cv.get_id();
let next_hop = cv.receiver().unwrap_or_default();
let response = match relay_router::route_relay(
&self.state,
RelaySource::Iota {
iota_id: self.iota_id,
},
cv,
)
.await
{
Ok(()) => CommunicationValue::new(CommunicationType::Success).with_id(request_id),
Err(error) => {
log_err!(
self.iota_id as i64,
PrintType::Iota,
"Relay routing failed for destination {}: {}",
next_hop,
error
);
CommunicationValue::new(relay_router::error_response_type(&error))
.with_id(request_id)
}
};
self.send_message(&response).await;
return;
}
if matches!(
crate::rho::relay_router::message_security_class(&cv),
crate::rho::relay_router::MessageSecurityClass::RelayOnly
) {
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidData, None)
.await;
return;
}
let msg_id = cv.get_id();
if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) {
if (task)(self.clone(), cv.clone()) {
@ -304,16 +323,6 @@ impl IotaConnection {
return;
}
// Handle forwarding to other Iotas or clients
let receiver_id = cv.get_receiver();
if (receiver_id != 0 && !self.get_user_ids().await.contains(&(receiver_id as u64)))
|| cv.is_type(CommunicationType::MessageOtherIota)
|| cv.is_type(CommunicationType::SendChat)
{
self.handle_forward_message(cv).await;
return;
}
if cv.is_type(CommunicationType::CompleteRegisterUser) {
// Registration carries the authenticated Iota ID separately so
// Omega can bind the allocation lease to this connection rather
@ -462,7 +471,14 @@ impl IotaConnection {
self.handle_omega_forward_without_sender(cv).await;
return;
}
self.forward_to_client(cv).await;
log_err!(
self.iota_id as i64,
PrintType::Iota,
"Rejected unsupported communication type {}",
cv.get_type()
);
self.send_error_response(cv.get_id(), CommunicationType::ErrorInvalidData, None)
.await;
}
#[allow(dead_code)]
@ -542,63 +558,6 @@ impl IotaConnection {
}
}
}
/// Handle message forwarding to other Iotas
async fn handle_forward_message(&self, cv: CommunicationValue) {
let receiver_id = cv.get_receiver();
let sender_id = cv.get_sender();
let my_user_ids = self.get_user_ids().await;
log_in!(
self.iota_id as i64,
PrintType::Iota,
"Authority check: sender_id={} receiver_id={} iota_user_ids={:?} msg_type={:?} msg_id={}",
sender_id,
receiver_id,
my_user_ids,
cv.get_type(),
cv.get_id()
);
if my_user_ids.contains(&(sender_id as u64)) {
if let Some(target_rho) = self.state.rho.get_for_user(receiver_id as i64).await {
target_rho.message_to_iota(cv).await;
} else {
if cv.is_type(CommunicationType::SetChatSecret) {
self.store_pending_chat_secret(cv.clone());
let success = CommunicationValue::new(CommunicationType::Success)
.with_id(cv.get_id())
.with_sender(cv.get_sender())
.with_receiver(cv.get_sender());
self.send_message(&success).await;
return;
}
let error = CommunicationValue::new(CommunicationType::ErrorNoIota)
.with_id(cv.get_id())
.with_sender(cv.get_sender());
self.send_message(&error).await;
}
} else {
log_err!(
self.iota_id as i64,
PrintType::Iota,
"Rejected client->iota forward: sender_id={} is not authorized for this iota. Known users={:?}",
sender_id,
my_user_ids
);
self.send_message(
&CommunicationValue::new(CommunicationType::ErrorInvalidUserId).add_typed_default(
DataType::ErrorType,
DataValue::Str(
"You are sending to another User without authority.".to_string(),
),
),
)
.await;
}
}
/// Handle GET_CHATS message
async fn handle_get_chats(&self, cv: CommunicationValue) {
let user_id = cv.get_sender();
@ -633,7 +592,7 @@ impl IotaConnection {
// Presence interest is the complete contact set, independent of
// whether this account currently participates in a call.
if let DataValue::Array(users) = cv.get_data(DataType::UserIds) {
if let Some(DataValue::Array(users)) = cv.get_data(DataType::UserIds) {
for user in users {
match user {
DataValue::SignedNumber(id) => interested_ids.push(*id as i64),
@ -733,13 +692,13 @@ impl IotaConnection {
// ============================
let enriched_contacts = if empty {
match cv.get_data(DataType::UserIds) {
DataValue::Array(arr) => DataValue::Array(arr.clone()),
Some(DataValue::Array(arr)) => DataValue::Array(arr.clone()),
_ => DataValue::Array(vec![]),
}
} else {
let mut enriched: Vec<DataValue> = Vec::new();
if let DataValue::Array(users) = cv.get_data(DataType::UserIds) {
if let Some(DataValue::Array(users)) = cv.get_data(DataType::UserIds) {
for user_val in users {
if let DataValue::Container(entries) = user_val {
let mut user_map: BTreeMap<DataTypeId, DataValue> =
@ -806,7 +765,8 @@ impl IotaConnection {
let mut output = response.clone();
let user_id = response.get_receiver();
for (key, value) in response.iter_typed_data() {
let typed_data: Vec<_> = response.iter_typed_data().collect();
for (key, value) in typed_data {
if key == Some(DataType::Contacts) {
if let Some(contacts) = value.as_array() {
let (contacts, global_calls) = self

View file

@ -2,6 +2,7 @@ pub mod app_connection;
pub mod client_connection;
pub mod connection;
pub mod iota_connection;
pub mod relay_router;
pub mod rho_connection;
pub mod rho_manager;
pub mod server;

409
src/rho/relay_router.rs Normal file
View file

@ -0,0 +1,409 @@
use std::{
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
time::Duration,
};
use mtp::codec::{CommunicationType, CommunicationValue, RelayError, forward_relay_frame};
use thiserror::Error;
use crate::{
app_state::AppState, omega::omega_connection::OmegaConnection, rho::rho_manager::RhoManager,
};
const TARGET_KIND_MASK: u64 = 0xC000_0000_0000_0000;
const TARGET_ID_MASK: u64 = (1_u64 << 48) - 1;
const USER_TARGET_KIND: u64 = 0x4000_0000_0000_0000;
const IOTA_TARGET_KIND: u64 = 0x8000_0000_0000_0000;
static NEXT_RELAY_FRAME_ID: AtomicU32 = AtomicU32::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouteTarget {
User(u64),
Iota(u64),
}
impl RouteTarget {
pub fn wire_id(self) -> Option<u64> {
let (kind, id) = match self {
Self::User(id) => (USER_TARGET_KIND, id),
Self::Iota(id) => (IOTA_TARGET_KIND, id),
};
(id > 0 && id <= TARGET_ID_MASK).then_some(kind | id)
}
pub fn from_wire_id(value: u64) -> Option<Self> {
let id = value & TARGET_ID_MASK;
if id == 0 || value & !(TARGET_KIND_MASK | TARGET_ID_MASK) != 0 {
return None;
}
match value & TARGET_KIND_MASK {
USER_TARGET_KIND => Some(Self::User(id)),
IOTA_TARGET_KIND => Some(Self::Iota(id)),
_ => None,
}
}
pub const fn id(self) -> u64 {
match self {
Self::User(id) | Self::Iota(id) => id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelaySource {
Client { iota_id: u64 },
Iota { iota_id: u64 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageSecurityClass {
RelayOnly,
AuthenticatedPeerControl,
AuthenticatedLocalRequest,
}
pub fn message_security_class(frame: &CommunicationValue) -> MessageSecurityClass {
const RELAY_ONLY_TYPES: &[CommunicationType] = &[
CommunicationType::MessageSend,
CommunicationType::MessageLive,
CommunicationType::MessageState,
CommunicationType::MessageEdit,
CommunicationType::MessageEditLive,
CommunicationType::MessageReactionAdd,
CommunicationType::MessageReactionRemove,
CommunicationType::MessageReactionLive,
CommunicationType::MessageDelete,
CommunicationType::MessageDeleteLive,
CommunicationType::MessageOtherIota,
CommunicationType::SetChatSecret,
CommunicationType::SendChat,
CommunicationType::SettingsSave,
CommunicationType::GlobalSettingsSave,
CommunicationType::AddConversation,
CommunicationType::AddCommunity,
CommunicationType::RemoveCommunity,
];
if RELAY_ONLY_TYPES.iter().any(|kind| frame.is_type(*kind)) {
MessageSecurityClass::RelayOnly
} else if frame.is_type(CommunicationType::GetChatSecret)
|| frame.is_type(CommunicationType::MessageGet)
|| frame.is_type(CommunicationType::MessagesGet)
{
MessageSecurityClass::AuthenticatedPeerControl
} else {
MessageSecurityClass::AuthenticatedLocalRequest
}
}
#[derive(Debug, Error)]
pub enum RelayRouteError {
#[error("relay has no next-hop receiver")]
MissingReceiver,
#[error("relay has invalid route target {0}")]
InvalidRouteTarget(u64),
#[error("client relay destination {actual} is not its associated Iota {expected}")]
InvalidClientRoute { expected: u64, actual: u64 },
#[error("relay destination Iota is not connected to this Omikron")]
DestinationIotaNotLocal,
#[error("relay destination client is offline")]
ClientOffline,
#[error("relay route resolves back to its source Iota")]
RouteLoop,
#[error("relay send failed: {0}")]
Send(String),
#[error(transparent)]
Relay(#[from] RelayError),
}
pub fn error_response_type(error: &RelayRouteError) -> CommunicationType {
match error {
RelayRouteError::DestinationIotaNotLocal | RelayRouteError::ClientOffline => {
CommunicationType::ErrorNoIota
}
RelayRouteError::Send(_) => CommunicationType::ErrorInternal,
RelayRouteError::MissingReceiver
| RelayRouteError::InvalidRouteTarget(_)
| RelayRouteError::InvalidClientRoute { .. }
| RelayRouteError::RouteLoop
| RelayRouteError::Relay(_) => CommunicationType::ErrorInvalidData,
}
}
pub async fn route_relay(
state: &Arc<AppState>,
source: RelaySource,
frame: CommunicationValue,
) -> Result<(), RelayRouteError> {
let (next_hop, frame) = prepare_frame(ensure_relay_frame_id(frame))?;
validate_source_next_hop(source, next_hop)?;
match source {
RelaySource::Client { iota_id } => {
let rho = state
.rho
.get_by_iota(
i64::try_from(iota_id)
.map_err(|_| RelayRouteError::InvalidRouteTarget(iota_id))?,
)
.await
.ok_or(RelayRouteError::DestinationIotaNotLocal)?;
let response = rho
.await_relay_to_iota(&frame)
.await
.map_err(RelayRouteError::Send)?;
route_response(response)
}
RelaySource::Iota { iota_id } => {
route_from_iota(&state.rho, &state.omega, iota_id, next_hop, frame).await
}
}
}
pub async fn route_from_omega(
rho: &RhoManager,
frame: CommunicationValue,
) -> Result<(), RelayRouteError> {
let (next_hop, frame) = prepare_frame(ensure_relay_frame_id(frame))?;
let RouteTarget::Iota(iota_id) = next_hop else {
return Err(RelayRouteError::InvalidRouteTarget(next_hop.id()));
};
let target = rho
.get_by_iota(
i64::try_from(iota_id).map_err(|_| RelayRouteError::InvalidRouteTarget(iota_id))?,
)
.await
.ok_or(RelayRouteError::DestinationIotaNotLocal)?;
let response = target
.await_relay_to_iota(&frame)
.await
.map_err(RelayRouteError::Send)?;
route_response(response)
}
async fn route_from_iota(
rho: &RhoManager,
omega: &OmegaConnection,
source_iota_id: u64,
next_hop: RouteTarget,
frame: CommunicationValue,
) -> Result<(), RelayRouteError> {
match next_hop {
RouteTarget::User(user_id) => {
let target = rho
.get_for_user(
i64::try_from(user_id)
.map_err(|_| RelayRouteError::InvalidRouteTarget(user_id))?,
)
.await
.ok_or(RelayRouteError::ClientOffline)?;
if !target.has_local_client(user_id).await {
return Err(RelayRouteError::ClientOffline);
}
target.send_relay_to_client(&frame).await.map_err(|error| {
if error == "client offline" {
RelayRouteError::ClientOffline
} else {
RelayRouteError::Send(error)
}
})
}
RouteTarget::Iota(iota_id) => {
if iota_id == source_iota_id {
return Err(RelayRouteError::RouteLoop);
}
if let Some(target) = rho
.get_by_iota(
i64::try_from(iota_id)
.map_err(|_| RelayRouteError::InvalidRouteTarget(iota_id))?,
)
.await
{
let response = target
.await_relay_to_iota(&frame)
.await
.map_err(RelayRouteError::Send)?;
return route_response(response);
}
let response = omega
.await_response(&frame, Some(Duration::from_secs(20)))
.await
.map_err(RelayRouteError::Send)?;
route_response(response)
}
}
}
fn route_response(response: CommunicationValue) -> Result<(), RelayRouteError> {
if response.is_type(CommunicationType::Success) {
Ok(())
} else {
Err(RelayRouteError::Send(format!(
"next Relay hop rejected the frame with {}",
response.get_type()
)))
}
}
pub 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)
}
fn prepare_frame(
frame: CommunicationValue,
) -> Result<(RouteTarget, CommunicationValue), RelayRouteError> {
let next_hop = frame.receiver().ok_or(RelayRouteError::MissingReceiver)?;
let target =
RouteTarget::from_wire_id(next_hop).ok_or(RelayRouteError::InvalidRouteTarget(next_hop))?;
let frame = forward_relay_frame(&frame, next_hop)?;
Ok((target, frame))
}
fn validate_source_next_hop(
source: RelaySource,
next_hop: RouteTarget,
) -> Result<(), RelayRouteError> {
match source {
RelaySource::Client { iota_id } => match next_hop {
RouteTarget::Iota(actual) if actual == iota_id => Ok(()),
_ => Err(RelayRouteError::InvalidClientRoute {
expected: iota_id,
actual: next_hop.id(),
}),
},
RelaySource::Iota { iota_id } => {
if next_hop == RouteTarget::Iota(iota_id) {
Err(RelayRouteError::RouteLoop)
} else {
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use mtp::codec::{CommunicationType, CommunicationValue, DataValue};
use super::{
RelayRouteError, RelaySource, RouteTarget, prepare_frame, validate_source_next_hop,
};
fn wire(target: RouteTarget) -> u64 {
let Some(value) = target.wire_id() else {
panic!("valid route target was rejected");
};
value
}
fn relay(receiver: Option<u64>) -> CommunicationValue {
let frame = CommunicationValue::new(CommunicationType::Relay)
.without_sender()
.with_payload(DataValue::Bytes(vec![1, 2, 3]));
receiver.map_or(frame.clone(), |id| frame.with_receiver(id))
}
#[test]
fn relay_requires_a_next_hop() {
assert!(matches!(
prepare_frame(relay(None)),
Err(RelayRouteError::MissingReceiver)
));
}
#[test]
fn relay_rejects_an_outer_sender() {
let frame = relay(Some(wire(RouteTarget::Iota(7)))).with_sender(9);
assert!(matches!(
prepare_frame(frame),
Err(RelayRouteError::Relay(
mtp::codec::RelayError::OuterSenderPresent
))
));
}
#[test]
fn relay_forwarding_preserves_payload_and_next_hop() {
let frame = relay(Some(wire(RouteTarget::Iota(7))));
let (next_hop, forwarded) = match prepare_frame(frame.clone()) {
Ok(value) => value,
Err(error) => panic!("valid relay was rejected: {error}"),
};
assert_eq!(next_hop, RouteTarget::Iota(7));
assert_eq!(forwarded.receiver(), Some(wire(RouteTarget::Iota(7))));
assert_eq!(forwarded.sender(), None);
assert_eq!(forwarded.payload(), frame.payload());
}
#[test]
fn non_relay_frames_are_rejected() {
let frame = CommunicationValue::new(CommunicationType::Ping)
.with_receiver(wire(RouteTarget::Iota(7)));
assert!(matches!(
prepare_frame(frame),
Err(RelayRouteError::Relay(mtp::codec::RelayError::NotRelay))
));
}
#[test]
fn client_cannot_address_another_iota() {
assert!(matches!(
validate_source_next_hop(RelaySource::Client { iota_id: 7 }, RouteTarget::Iota(8)),
Err(RelayRouteError::InvalidClientRoute {
expected: 7,
actual: 8
})
));
}
#[test]
fn client_cannot_address_a_user() {
assert!(matches!(
validate_source_next_hop(RelaySource::Client { iota_id: 7 }, RouteTarget::User(8)),
Err(RelayRouteError::InvalidClientRoute {
expected: 7,
actual: 8
})
));
}
#[test]
fn iota_cannot_route_to_itself() {
assert!(matches!(
validate_source_next_hop(RelaySource::Iota { iota_id: 7 }, RouteTarget::Iota(7)),
Err(RelayRouteError::RouteLoop)
));
}
#[tokio::test]
async fn omega_does_not_forward_to_another_omikron() {
let rho = crate::rho::rho_manager::RhoManager::new();
let result = super::route_from_omega(&rho, relay(Some(wire(RouteTarget::Iota(7))))).await;
assert!(matches!(
result,
Err(RelayRouteError::DestinationIotaNotLocal)
));
}
#[test]
fn route_target_namespace_is_explicit() {
assert_eq!(
RouteTarget::from_wire_id(wire(RouteTarget::User(7))),
Some(RouteTarget::User(7))
);
assert_eq!(
RouteTarget::from_wire_id(wire(RouteTarget::Iota(7))),
Some(RouteTarget::Iota(7))
);
assert_eq!(RouteTarget::from_wire_id(7), None);
}
}

View file

@ -1,8 +1,15 @@
use super::{client_connection::ClientConnection, iota_connection::IotaConnection};
use crate::{log_err, rho::app_connection::AppConnection};
use super::relay_router::RouteTarget;
use crate::{
log_err,
rho::{
app_connection::AppConnection,
connection::{MtpValueCompat, OptionalDataValueCompat},
},
};
use dashmap::DashMap;
use mtp::codec::{CommunicationValue, DataType};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
@ -91,7 +98,8 @@ impl RhoConnection {
for client in clients {
client.clear_rho_connection().await;
}
self.client_connections.retain(|(id, _), _| *id != user_id as u64);
self.client_connections
.retain(|(id, _), _| *id != user_id as u64);
}
#[allow(unused)]
@ -265,11 +273,62 @@ impl RhoConnection {
}
}
/// Send message to Iota
pub async fn message_to_iota(&self, cv: CommunicationValue) {
self.iota_connection.send_message(&cv).await;
}
pub async fn has_local_client(&self, user_id: u64) -> bool {
self.client_connections
.iter()
.any(|entry| entry.key().0 == user_id)
}
pub async fn send_relay_to_client(&self, cv: &CommunicationValue) -> Result<(), String> {
let receiver_id = cv
.receiver()
.and_then(RouteTarget::from_wire_id)
.and_then(|target| match target {
RouteTarget::User(user_id) => Some(user_id),
RouteTarget::Iota(_) => None,
})
.ok_or_else(|| "client offline".to_string())?;
let connections = self
.get_client_connections_for_user(receiver_id as i64)
.await;
if connections.is_empty() {
return Err("client offline".to_string());
}
let mut send_error = None;
for connection in connections {
if let Err(error) = connection.send_relay(cv).await {
send_error = Some(error);
}
}
send_error.map_or(Ok(()), Err)
}
pub async fn send_relay_to_iota(&self, cv: &CommunicationValue) -> Result<(), String> {
self.iota_connection.send_relay(cv).await
}
pub async fn await_relay_to_iota(
&self,
cv: &CommunicationValue,
) -> Result<CommunicationValue, String> {
self.iota_connection
.clone()
.await_response(cv, Some(std::time::Duration::from_secs(20)))
.await
}
pub async fn forward_relay_ack(&self, user_id: u64, frame_id: u32) {
let acknowledgement = CommunicationValue::new(CommunicationType::Success)
.with_id(frame_id)
.add_typed_default(DataType::UserId, DataValue::UnsignedNumber(user_id.into()));
self.iota_connection.send_message(&acknowledgement).await;
}
/// Check if this RhoConnection contains a specific user ID
#[allow(dead_code)]
pub async fn contains_user(&self, user_id: &i64) -> bool {

View file

@ -66,7 +66,10 @@ impl RhoManager {
)
.await;
let replacement = user_ids.iter().copied().collect::<std::collections::HashSet<_>>();
let replacement = user_ids
.iter()
.copied()
.collect::<std::collections::HashSet<_>>();
for user_id in previous_users {
if !replacement.contains(&user_id) {
rho.detach_user_clients(user_id).await;

View file

@ -9,7 +9,7 @@ use crate::{
app_state::AppState,
log, log_err,
omega::omega_connection::OmegaConnection,
rho::connection::GeneralConnection,
rho::connection::{GeneralConnection, OptionalDataValueCompat},
util::{file_util::load_file_vec, logger::PrintType},
};
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue};
@ -98,7 +98,7 @@ pub async fn complete_register(
};
match response.get_data(DataType::IotaId) {
DataValue::SignedNumber(id) => *id as u64,
Some(DataValue::SignedNumber(id)) => *id as u64,
_ => 0,
}
}

View file

@ -7,7 +7,9 @@ use std::{
};
use ansi_term::Color;
use mtp::codec::{CommunicationValue, DataTypeId, DataValue, Version};
use mtp::codec::{CommunicationType, CommunicationValue, DataTypeId, DataValue, Version};
use crate::rho::connection::MtpValueCompat;
static LOGGER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
@ -179,11 +181,16 @@ pub fn format_cv(cv: &CommunicationValue) -> String {
.unwrap_or_else(|| cv.get_type().to_string());
parts.push(format!("{} (id={})", comm_type, cv.get_id()));
let data = cv.data();
if cv.is_type(CommunicationType::Relay) {
parts.push("<opaque relay payload>".to_string());
return parts.join(": ");
}
let data = cv.data().unwrap_or(&[]);
let formated_data = format_data_container(
data.iter().map(|(k, v)| (*k, v.clone())).collect(),
Version(1, 0),
Version(3, 0),
);
parts.push(format!("{}", formated_data));