[Fix] Connection Management
This commit is contained in:
parent
9e9e3597da
commit
3f2ac18333
122 changed files with 19970 additions and 5263 deletions
|
|
@ -4,9 +4,17 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.89"
|
||||
base64 = "0.22.1"
|
||||
iota-auth = { path = "../iota-auth" }
|
||||
iota-identity = { path = "../iota-identity" }
|
||||
iota-logger = { path = "../iota-logger" }
|
||||
iota-storage = { path = "../iota-storage" }
|
||||
iota-util = { path = "../iota-util" }
|
||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a135d0f0c2b35147011905f8ee0fc37050f69a6c", features = ["crypto"] }
|
||||
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "1f19a0d897c265d1e3f590a876f95e766ff99318", features = ["crypto"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1.50.0", features = ["rt"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.50.0", features = ["macros", "rt"] }
|
||||
|
|
|
|||
|
|
@ -1,36 +1,89 @@
|
|||
use mtp::codec::CommunicationValue;
|
||||
use std::future::Future;
|
||||
use async_trait::async_trait;
|
||||
use iota_identity::{AuthorityId, IotaNodeId};
|
||||
use mtp::codec::{CommunicationType, CommunicationValue};
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Unified interface for all connection types (Omikron, Direct, future modes).
|
||||
///
|
||||
/// Provides the common messaging API that the rest of the codebase uses,
|
||||
/// regardless of whether the connection goes through Omikron or is direct.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ConnectionError {
|
||||
Disconnected(String),
|
||||
Timeout(String),
|
||||
Protocol(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ConnectionError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Disconnected(message) | Self::Timeout(message) | Self::Protocol(message) => {
|
||||
formatter.write_str(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ConnectionError {}
|
||||
|
||||
#[async_trait]
|
||||
pub trait ConnectionHandler: Send + Sync {
|
||||
/// Send a message to the remote end.
|
||||
fn send_message(
|
||||
&self,
|
||||
cv: &CommunicationValue,
|
||||
) -> impl Future<Output = Result<(), String>> + Send;
|
||||
|
||||
/// Send a message and wait for a correlated response.
|
||||
///
|
||||
/// The implementation correlates requests/responses by message ID and
|
||||
/// enforces the given `timeout`. Returns an error on timeout or if the
|
||||
/// connection drops while waiting.
|
||||
fn await_response(
|
||||
async fn send_message(&self, value: &CommunicationValue) -> Result<(), ConnectionError>;
|
||||
|
||||
async fn await_response(
|
||||
&self,
|
||||
cv: &CommunicationValue,
|
||||
value: &CommunicationValue,
|
||||
timeout: Option<Duration>,
|
||||
) -> impl Future<Output = Result<CommunicationValue, String>> + Send;
|
||||
) -> Result<CommunicationValue, ConnectionError>;
|
||||
|
||||
async fn is_connected(&self) -> bool;
|
||||
|
||||
/// Returns `true` when the connection is alive and ready for traffic.
|
||||
fn is_connected(&self) -> impl Future<Output = bool> + Send;
|
||||
async fn is_identified(&self) -> bool;
|
||||
|
||||
/// Returns `true` when the connection has completed identification /
|
||||
/// registration and is fully operational.
|
||||
fn is_identified(&self) -> impl Future<Output = bool> + Send;
|
||||
async fn stop(&self);
|
||||
}
|
||||
|
||||
/// Gracefully tear down the connection.
|
||||
fn stop(&self) -> impl Future<Output = ()> + Send;
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RouteDestination {
|
||||
Iota(IotaNodeId),
|
||||
LegacyOmegaIota { omega: AuthorityId, iota_id: u64 },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RouteOutcome {
|
||||
Accepted {
|
||||
relay_message_id: String,
|
||||
destination_accepted_at: i64,
|
||||
},
|
||||
Rejected {
|
||||
response_type: CommunicationType,
|
||||
},
|
||||
Retryable {
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RouteError {
|
||||
NoRoute(IotaNodeId),
|
||||
Delivery(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for RouteError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::NoRoute(destination) => {
|
||||
write!(formatter, "no route to Iota {}", destination.as_str())
|
||||
}
|
||||
Self::Delivery(message) => formatter.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RouteError {}
|
||||
|
||||
#[async_trait]
|
||||
pub trait PeerRouter: Send + Sync {
|
||||
async fn route(
|
||||
&self,
|
||||
destination: &RouteDestination,
|
||||
frame: CommunicationValue,
|
||||
) -> Result<RouteOutcome, RouteError>;
|
||||
}
|
||||
|
|
|
|||
438
iota-connection/src/federated_relay.rs
Normal file
438
iota-connection/src/federated_relay.rs
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use iota_identity::{
|
||||
IdentityError, IdentityResolver, IotaNodeId, PrincipalId, ResolutionContext, ResolvedPrincipal,
|
||||
verify_dual_signature,
|
||||
};
|
||||
use iota_util::mtp_compat::OptionalDataValueExt;
|
||||
use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue, TypeMap};
|
||||
use mtp::crypto::{Keyring, SignatureScheme};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const SIGNING_DOMAIN: &[u8] = b"tensamin-federated-relay:v2";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FederatedRelayV2 {
|
||||
pub signer: PrincipalId,
|
||||
pub recipient: PrincipalId,
|
||||
pub destination: IotaNodeId,
|
||||
pub message_id: String,
|
||||
pub created_at: u64,
|
||||
pub content: CommunicationValue,
|
||||
pub signature: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VerifiedFederatedRelayV2 {
|
||||
pub signer: ResolvedPrincipal,
|
||||
pub recipient: ResolvedPrincipal,
|
||||
pub destination: IotaNodeId,
|
||||
pub message_id: String,
|
||||
pub created_at: u64,
|
||||
pub content: CommunicationValue,
|
||||
pub frame: CommunicationValue,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FederatedRelayWireV2 {
|
||||
version: u32,
|
||||
signer: PrincipalId,
|
||||
recipient: PrincipalId,
|
||||
destination: IotaNodeId,
|
||||
message_id: String,
|
||||
created_at: u64,
|
||||
type_map_version: String,
|
||||
content: String,
|
||||
signature: String,
|
||||
}
|
||||
|
||||
impl FederatedRelayV2 {
|
||||
pub fn sign(
|
||||
signer: PrincipalId,
|
||||
recipient: PrincipalId,
|
||||
destination: IotaNodeId,
|
||||
message_id: String,
|
||||
created_at: u64,
|
||||
content: CommunicationValue,
|
||||
keyring: &Keyring,
|
||||
) -> Result<Self, IdentityError> {
|
||||
validate_fields(&signer, &recipient, &message_id, &content)?;
|
||||
let signer_impl = mtp::crypto::DualSigner::new(
|
||||
&keyring.sig_cl_secret_key,
|
||||
&keyring.sig_pq_secret_key,
|
||||
&keyring.sig_pq_public_key,
|
||||
)
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
|
||||
let signature = signer_impl
|
||||
.sign(&canonical_bytes(
|
||||
&signer,
|
||||
&recipient,
|
||||
&destination,
|
||||
&message_id,
|
||||
created_at,
|
||||
&content,
|
||||
)?)
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
|
||||
Ok(Self {
|
||||
signer,
|
||||
recipient,
|
||||
destination,
|
||||
message_id,
|
||||
created_at,
|
||||
content,
|
||||
signature,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_frame(self, frame_id: u32) -> Result<CommunicationValue, IdentityError> {
|
||||
let type_map = self.content.type_map().ok_or_else(|| {
|
||||
IdentityError::InvalidDescriptor("federated relay content has no type map".into())
|
||||
})?;
|
||||
let content = self
|
||||
.content
|
||||
.to_bytes()
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
|
||||
let wire = FederatedRelayWireV2 {
|
||||
version: 2,
|
||||
signer: self.signer,
|
||||
recipient: self.recipient,
|
||||
destination: self.destination,
|
||||
message_id: self.message_id,
|
||||
created_at: self.created_at,
|
||||
type_map_version: type_map.version.to_string(),
|
||||
content: STANDARD.encode(content),
|
||||
signature: STANDARD.encode(self.signature),
|
||||
};
|
||||
let payload = serde_json::to_vec(&wire)
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
|
||||
Ok(CommunicationValue::new(CommunicationType::Relay)
|
||||
.with_id(frame_id)
|
||||
.add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(2))
|
||||
.add_typed_default(DataType::SecurePayload, DataValue::Bytes(payload)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn decode_and_verify(
|
||||
frame: CommunicationValue,
|
||||
identities: &dyn IdentityResolver,
|
||||
local_node: &IotaNodeId,
|
||||
) -> Result<VerifiedFederatedRelayV2, IdentityError> {
|
||||
decode_and_verify_for_ingress(frame, identities, Some(local_node)).await
|
||||
}
|
||||
|
||||
pub async fn decode_and_verify_for_ingress(
|
||||
frame: CommunicationValue,
|
||||
identities: &dyn IdentityResolver,
|
||||
expected_destination: Option<&IotaNodeId>,
|
||||
) -> Result<VerifiedFederatedRelayV2, IdentityError> {
|
||||
if !frame.is_type(CommunicationType::Relay)
|
||||
|| frame.get_data(DataType::VersionNumber).as_number() != Some(2)
|
||||
{
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"frame is not Federated Relay V2".into(),
|
||||
));
|
||||
}
|
||||
let payload = frame
|
||||
.get_data(DataType::SecurePayload)
|
||||
.and_then(|value| match value {
|
||||
DataValue::Bytes(bytes) => Some(bytes.as_slice()),
|
||||
_ => None,
|
||||
})
|
||||
.ok_or_else(|| IdentityError::InvalidDescriptor("Relay V2 payload is missing".into()))?;
|
||||
let wire: FederatedRelayWireV2 = serde_json::from_slice(payload)
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
|
||||
if wire.version != 2
|
||||
|| expected_destination.is_some_and(|expected| &wire.destination != expected)
|
||||
{
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"Relay V2 destination does not match this Iota".into(),
|
||||
));
|
||||
}
|
||||
let type_map_version =
|
||||
mtp::type_map::Version::parse(&wire.type_map_version).ok_or_else(|| {
|
||||
IdentityError::InvalidDescriptor("Relay V2 type-map version is invalid".into())
|
||||
})?;
|
||||
let content_bytes = STANDARD
|
||||
.decode(&wire.content)
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
|
||||
let content =
|
||||
CommunicationValue::from_bytes_with(&content_bytes, &TypeMap::new(type_map_version))
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
|
||||
validate_fields(&wire.signer, &wire.recipient, &wire.message_id, &content)?;
|
||||
let signature = STANDARD
|
||||
.decode(&wire.signature)
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?;
|
||||
let context = ResolutionContext {
|
||||
allow_network: true,
|
||||
..ResolutionContext::default()
|
||||
};
|
||||
let signer = identities
|
||||
.resolve_principal_with_context(&wire.signer, &context)
|
||||
.await?;
|
||||
let recipient = identities
|
||||
.resolve_principal_with_context(&wire.recipient, &context)
|
||||
.await?;
|
||||
if !matches!(
|
||||
&recipient.home,
|
||||
iota_identity::PrincipalHome::Iota(home) if home == &wire.destination
|
||||
) {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"Relay V2 destination does not match recipient home".into(),
|
||||
));
|
||||
}
|
||||
let bytes = canonical_bytes(
|
||||
&wire.signer,
|
||||
&wire.recipient,
|
||||
&wire.destination,
|
||||
&wire.message_id,
|
||||
wire.created_at,
|
||||
&content,
|
||||
)?;
|
||||
if !signer
|
||||
.public_keys
|
||||
.iter()
|
||||
.any(|key| verify_dual_signature(key, &bytes, &signature).is_ok())
|
||||
{
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"Relay V2 user signature is invalid".into(),
|
||||
));
|
||||
}
|
||||
Ok(VerifiedFederatedRelayV2 {
|
||||
signer,
|
||||
recipient,
|
||||
destination: wire.destination,
|
||||
message_id: wire.message_id,
|
||||
created_at: wire.created_at,
|
||||
content,
|
||||
frame,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_fields(
|
||||
signer: &PrincipalId,
|
||||
recipient: &PrincipalId,
|
||||
message_id: &str,
|
||||
content: &CommunicationValue,
|
||||
) -> Result<(), IdentityError> {
|
||||
if signer == recipient {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"Relay V2 signer and recipient are identical".into(),
|
||||
));
|
||||
}
|
||||
if message_id.is_empty() || message_id.len() > 256 {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"Relay V2 message ID is invalid".into(),
|
||||
));
|
||||
}
|
||||
if content.type_map().is_none() || content.is_type(CommunicationType::Relay) {
|
||||
return Err(IdentityError::InvalidDescriptor(
|
||||
"Relay V2 content is invalid".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn canonical_bytes(
|
||||
signer: &PrincipalId,
|
||||
recipient: &PrincipalId,
|
||||
destination: &IotaNodeId,
|
||||
message_id: &str,
|
||||
created_at: u64,
|
||||
content: &CommunicationValue,
|
||||
) -> Result<Vec<u8>, IdentityError> {
|
||||
let mut bytes = SIGNING_DOMAIN.to_vec();
|
||||
push(&mut bytes, signer.authority.as_str().as_bytes())?;
|
||||
bytes.extend_from_slice(&signer.user_id.to_be_bytes());
|
||||
push(&mut bytes, recipient.authority.as_str().as_bytes())?;
|
||||
bytes.extend_from_slice(&recipient.user_id.to_be_bytes());
|
||||
push(&mut bytes, destination.as_str().as_bytes())?;
|
||||
push(&mut bytes, message_id.as_bytes())?;
|
||||
bytes.extend_from_slice(&created_at.to_be_bytes());
|
||||
push(
|
||||
&mut bytes,
|
||||
&content
|
||||
.to_bytes()
|
||||
.map_err(|error| IdentityError::InvalidDescriptor(error.to_string()))?,
|
||||
)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn push(output: &mut Vec<u8>, value: &[u8]) -> Result<(), IdentityError> {
|
||||
let length = u32::try_from(value.len())
|
||||
.map_err(|_| IdentityError::InvalidDescriptor("Relay V2 field is too large".into()))?;
|
||||
output.extend_from_slice(&length.to_be_bytes());
|
||||
output.extend_from_slice(value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use iota_identity::{
|
||||
AuthorityId, PrincipalHandle, PrincipalHome, PublicKeyBundle, ResolvedPrincipal,
|
||||
UserAddress,
|
||||
};
|
||||
|
||||
struct Resolver {
|
||||
principals: Vec<ResolvedPrincipal>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl IdentityResolver for Resolver {
|
||||
async fn resolve_address(
|
||||
&self,
|
||||
_: &UserAddress,
|
||||
_: &ResolutionContext,
|
||||
) -> Result<ResolvedPrincipal, IdentityError> {
|
||||
Err(IdentityError::NotFound)
|
||||
}
|
||||
|
||||
async fn resolve_principal(
|
||||
&self,
|
||||
principal: &PrincipalId,
|
||||
) -> Result<ResolvedPrincipal, IdentityError> {
|
||||
self.principals
|
||||
.iter()
|
||||
.find(|candidate| &candidate.principal == principal)
|
||||
.cloned()
|
||||
.ok_or(IdentityError::NotFound)
|
||||
}
|
||||
|
||||
async fn signing_keys(
|
||||
&self,
|
||||
principal: &PrincipalId,
|
||||
_: &ResolutionContext,
|
||||
) -> Result<Vec<PublicKeyBundle>, IdentityError> {
|
||||
self.resolve_principal(principal)
|
||||
.await
|
||||
.map(|principal| principal.public_keys)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolved(
|
||||
authority: &AuthorityId,
|
||||
user_id: u64,
|
||||
handle: i64,
|
||||
key: PublicKeyBundle,
|
||||
home: IotaNodeId,
|
||||
) -> ResolvedPrincipal {
|
||||
ResolvedPrincipal {
|
||||
principal: PrincipalId {
|
||||
authority: authority.clone(),
|
||||
user_id,
|
||||
},
|
||||
handle: PrincipalHandle(handle),
|
||||
username: None,
|
||||
public_keys: vec![key],
|
||||
home: PrincipalHome::Iota(home),
|
||||
descriptor_revision: 1,
|
||||
valid_until: None,
|
||||
resolved_at: 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn relay() -> (
|
||||
CommunicationValue,
|
||||
Resolver,
|
||||
IotaNodeId,
|
||||
AuthorityId,
|
||||
AuthorityId,
|
||||
) {
|
||||
let signer_node =
|
||||
iota_identity::LocalNodeIdentity::from_keyring(Keyring::generate()).unwrap();
|
||||
let recipient_node =
|
||||
iota_identity::LocalNodeIdentity::from_keyring(Keyring::generate()).unwrap();
|
||||
let signer = Keyring::generate();
|
||||
let recipient = Keyring::generate();
|
||||
let signer_principal = PrincipalId {
|
||||
authority: signer_node.authority_id().clone(),
|
||||
user_id: 7,
|
||||
};
|
||||
let recipient_principal = PrincipalId {
|
||||
authority: recipient_node.authority_id().clone(),
|
||||
user_id: 7,
|
||||
};
|
||||
let content = CommunicationValue::new(CommunicationType::MessageSend)
|
||||
.add_typed_default(DataType::Content, DataValue::Str("ciphertext".into()))
|
||||
.add_typed_default(DataType::SendTime, DataValue::SignedNumber(10))
|
||||
.add_typed_default(DataType::VersionNumber, DataValue::SignedNumber(1));
|
||||
let frame = FederatedRelayV2::sign(
|
||||
signer_principal.clone(),
|
||||
recipient_principal.clone(),
|
||||
recipient_node.node_id().clone(),
|
||||
"same-id".into(),
|
||||
10,
|
||||
content,
|
||||
&signer,
|
||||
)
|
||||
.unwrap()
|
||||
.into_frame(5)
|
||||
.unwrap();
|
||||
let resolver = Resolver {
|
||||
principals: vec![
|
||||
resolved(
|
||||
signer_node.authority_id(),
|
||||
7,
|
||||
1,
|
||||
signer.public_key_bundle(),
|
||||
signer_node.node_id().clone(),
|
||||
),
|
||||
resolved(
|
||||
recipient_node.authority_id(),
|
||||
7,
|
||||
2,
|
||||
recipient.public_key_bundle(),
|
||||
recipient_node.node_id().clone(),
|
||||
),
|
||||
],
|
||||
};
|
||||
(
|
||||
frame,
|
||||
resolver,
|
||||
recipient_node.node_id().clone(),
|
||||
signer_node.authority_id().clone(),
|
||||
recipient_node.authority_id().clone(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn equal_user_ids_from_different_iotas_verify_as_distinct_principals() {
|
||||
let (frame, resolver, node, signer_authority, recipient_authority) = relay();
|
||||
let verified = decode_and_verify(frame, &resolver, &node).await.unwrap();
|
||||
assert_eq!(verified.signer.principal.user_id, 7);
|
||||
assert_eq!(verified.recipient.principal.user_id, 7);
|
||||
assert_eq!(verified.signer.principal.authority, signer_authority);
|
||||
assert_eq!(verified.recipient.principal.authority, recipient_authority);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn changing_either_authority_invalidates_relay_signature() {
|
||||
for field in ["signer", "recipient"] {
|
||||
let (mut frame, mut resolver, node, _, _) = relay();
|
||||
let payload = frame
|
||||
.get_data(DataType::SecurePayload)
|
||||
.and_then(|value| match value {
|
||||
DataValue::Bytes(bytes) => Some(bytes.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let mut wire: FederatedRelayWireV2 = serde_json::from_slice(&payload).unwrap();
|
||||
let changed = AuthorityId::new("authority:iota:v1:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap();
|
||||
if field == "signer" {
|
||||
resolver.principals[0].principal.authority = changed.clone();
|
||||
wire.signer.authority = changed;
|
||||
} else {
|
||||
resolver.principals[1].principal.authority = changed.clone();
|
||||
wire.recipient.authority = changed;
|
||||
}
|
||||
frame = CommunicationValue::new(CommunicationType::Relay)
|
||||
.with_id(5)
|
||||
.add_typed_default(DataType::VersionNumber, DataValue::UnsignedNumber(2))
|
||||
.add_typed_default(
|
||||
DataType::SecurePayload,
|
||||
DataValue::Bytes(serde_json::to_vec(&wire).unwrap()),
|
||||
);
|
||||
assert!(decode_and_verify(frame, &resolver, &node).await.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
pub mod connection_handler;
|
||||
pub mod federated_relay;
|
||||
pub mod message_common;
|
||||
pub mod message_handlers;
|
||||
pub mod relay;
|
||||
pub mod relay_service;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ fn next_notification_id() -> u32 {
|
|||
pub struct MessageMutation {
|
||||
pub sender_id: i64,
|
||||
pub partner_id: i64,
|
||||
pub partner_principal: iota_identity::PrincipalHandle,
|
||||
pub sender_principal: iota_identity::PrincipalHandle,
|
||||
pub send_time: i64,
|
||||
}
|
||||
|
||||
|
|
@ -77,10 +79,20 @@ pub fn message_mutation(cv: &CommunicationValue) -> Result<MessageMutation, Comm
|
|||
let send_time = data_i64(cv, DataType::SendTime)
|
||||
.filter(|time| *time > 0)
|
||||
.ok_or_else(|| error_response(cv, CommunicationType::ErrorInvalidData))?;
|
||||
let partner_principal = chats_util::get_user(sender_id, partner_id)
|
||||
.map_err(|_| error_response(cv, CommunicationType::ErrorInternal))?
|
||||
.and_then(|contact| contact.principal)
|
||||
.ok_or_else(|| error_response(cv, CommunicationType::ErrorNotFound))?;
|
||||
let sender_principal = iota_storage::identity::SqlitePrincipalStore
|
||||
.principal_for_local_user(iota_identity::LocalUserId(sender_id))
|
||||
.map_err(|_| error_response(cv, CommunicationType::ErrorInternal))?
|
||||
.ok_or_else(|| error_response(cv, CommunicationType::ErrorNotFound))?;
|
||||
|
||||
Ok(MessageMutation {
|
||||
sender_id,
|
||||
partner_id,
|
||||
partner_principal,
|
||||
sender_principal,
|
||||
send_time,
|
||||
})
|
||||
}
|
||||
|
|
@ -169,6 +181,8 @@ fn validate_relay_identity(
|
|||
pub fn apply_verified_relay_content(
|
||||
context: &VerifiedRelayContext,
|
||||
content: &VerifiedRelayContent,
|
||||
signer_principal: iota_identity::PrincipalHandle,
|
||||
recipient_principal: iota_identity::PrincipalHandle,
|
||||
accepted_at: i64,
|
||||
storage_owner: i64,
|
||||
sent_by_self: bool,
|
||||
|
|
@ -205,6 +219,7 @@ pub fn apply_verified_relay_content(
|
|||
return chat_files::change_message_state_by_relay_id(
|
||||
storage_owner,
|
||||
recipient_id,
|
||||
recipient_principal,
|
||||
relay_message_id,
|
||||
state,
|
||||
)
|
||||
|
|
@ -213,8 +228,10 @@ pub fn apply_verified_relay_content(
|
|||
chat_files::record_message_receipt(
|
||||
storage_owner,
|
||||
recipient_id,
|
||||
recipient_principal,
|
||||
relay_message_id,
|
||||
sender_id,
|
||||
signer_principal,
|
||||
&context.message_id,
|
||||
state,
|
||||
event_at,
|
||||
|
|
@ -251,15 +268,25 @@ pub fn apply_verified_relay_content(
|
|||
"Relay MessageSend identity does not match its protected message ID".into(),
|
||||
);
|
||||
}
|
||||
let chat_id = if sender_id < recipient_id {
|
||||
let chat_id = e2ee_storage::principal_chat_id(signer_principal, recipient_principal)
|
||||
.ok_or_else(|| "Relay MessageSend has an invalid principal pair".to_string())?;
|
||||
let legacy_chat_id = if sender_id < recipient_id {
|
||||
format!("{sender_id}:{recipient_id}")
|
||||
} else {
|
||||
format!("{recipient_id}:{sender_id}")
|
||||
};
|
||||
e2ee_storage::migrate_chat_secret_namespace(
|
||||
&storage_owner.to_string(),
|
||||
&legacy_chat_id,
|
||||
&format!("chat:{legacy_chat_id}:main"),
|
||||
&chat_id,
|
||||
&e2ee_storage::principal_secret_id(&chat_id),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let latest_secret = e2ee_storage::get_chat_secret(ChatSecretQuery {
|
||||
user_id: storage_owner.to_string(),
|
||||
chat_id: chat_id.clone(),
|
||||
secret_id: Some(format!("chat:{chat_id}:main")),
|
||||
secret_id: Some(e2ee_storage::principal_secret_id(&chat_id)),
|
||||
version: None,
|
||||
})
|
||||
.map_err(|error| error.to_string())?
|
||||
|
|
@ -269,6 +296,7 @@ pub fn apply_verified_relay_content(
|
|||
}
|
||||
chat_files::add_message(chat_files::NewMessage {
|
||||
relay_signer_id: sender_id,
|
||||
relay_signer_principal: signer_principal,
|
||||
relay_message_id: &context.message_id,
|
||||
authored_at: created_at,
|
||||
send_time,
|
||||
|
|
@ -278,6 +306,11 @@ pub fn apply_verified_relay_content(
|
|||
} else {
|
||||
sender_id
|
||||
},
|
||||
external_principal: if sent_by_self {
|
||||
recipient_principal
|
||||
} else {
|
||||
signer_principal
|
||||
},
|
||||
sent_by_self,
|
||||
content: message,
|
||||
height,
|
||||
|
|
@ -305,18 +338,25 @@ pub fn apply_verified_relay_content(
|
|||
} else {
|
||||
sender_id
|
||||
};
|
||||
let external_principal = if sent_by_self {
|
||||
recipient_principal
|
||||
} else {
|
||||
signer_principal
|
||||
};
|
||||
let result = if sent_by_self {
|
||||
chat_files::edit_message(
|
||||
chat_files::edit_message_for_principal(
|
||||
storage_owner,
|
||||
external_user,
|
||||
external_principal,
|
||||
send_time,
|
||||
sender_id,
|
||||
message,
|
||||
)
|
||||
} else {
|
||||
chat_files::apply_remote_edit(
|
||||
chat_files::apply_remote_edit_for_principal(
|
||||
storage_owner,
|
||||
external_user,
|
||||
external_principal,
|
||||
send_time,
|
||||
sender_id,
|
||||
message,
|
||||
|
|
@ -336,20 +376,29 @@ pub fn apply_verified_relay_content(
|
|||
} else {
|
||||
sender_id
|
||||
};
|
||||
let external_principal = if sent_by_self {
|
||||
recipient_principal
|
||||
} else {
|
||||
signer_principal
|
||||
};
|
||||
let result = if content.message_type == CommunicationType::MessageReactionAdd {
|
||||
chat_files::add_reaction(
|
||||
chat_files::add_reaction_for_principal(
|
||||
storage_owner,
|
||||
external_user,
|
||||
external_principal,
|
||||
send_time,
|
||||
sender_id,
|
||||
signer_principal,
|
||||
reaction,
|
||||
)
|
||||
} else {
|
||||
chat_files::remove_reaction(
|
||||
chat_files::remove_reaction_for_principal(
|
||||
storage_owner,
|
||||
external_user,
|
||||
external_principal,
|
||||
send_time,
|
||||
sender_id,
|
||||
signer_principal,
|
||||
reaction,
|
||||
)
|
||||
};
|
||||
|
|
@ -364,10 +413,26 @@ pub fn apply_verified_relay_content(
|
|||
} else {
|
||||
sender_id
|
||||
};
|
||||
let result = if sent_by_self {
|
||||
chat_files::delete_message(storage_owner, external_user, send_time)
|
||||
let external_principal = if sent_by_self {
|
||||
recipient_principal
|
||||
} else {
|
||||
chat_files::apply_remote_delete(storage_owner, external_user, send_time, sender_id)
|
||||
signer_principal
|
||||
};
|
||||
let result = if sent_by_self {
|
||||
chat_files::delete_message_for_principal(
|
||||
storage_owner,
|
||||
external_user,
|
||||
external_principal,
|
||||
send_time,
|
||||
)
|
||||
} else {
|
||||
chat_files::apply_remote_delete_for_principal(
|
||||
storage_owner,
|
||||
external_user,
|
||||
external_principal,
|
||||
send_time,
|
||||
sender_id,
|
||||
)
|
||||
};
|
||||
result.map_err(|error| error.to_string())
|
||||
}
|
||||
|
|
@ -434,10 +499,23 @@ pub fn apply_verified_relay_content(
|
|||
if recipient_ids != expected_recipient_ids {
|
||||
return Err("Relay SetChatSecret recipients do not match relay identities".into());
|
||||
}
|
||||
let principal_chat_id =
|
||||
e2ee_storage::principal_chat_id(signer_principal, recipient_principal).ok_or_else(
|
||||
|| "Relay SetChatSecret has an invalid principal pair".to_string(),
|
||||
)?;
|
||||
let principal_secret_id = e2ee_storage::principal_secret_id(&principal_chat_id);
|
||||
e2ee_storage::migrate_chat_secret_namespace(
|
||||
&storage_owner.to_string(),
|
||||
&chat_id,
|
||||
&secret_id,
|
||||
&principal_chat_id,
|
||||
&principal_secret_id,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
e2ee_storage::put_chat_secret(e2ee_storage::StoredChatSecret {
|
||||
user_id: storage_owner.to_string(),
|
||||
chat_id,
|
||||
secret_id,
|
||||
chat_id: principal_chat_id,
|
||||
secret_id: principal_secret_id,
|
||||
version,
|
||||
encrypted_secret: recipient.encrypted_secret.clone(),
|
||||
kem_ciphertext: recipient.kem_ciphertext.clone(),
|
||||
|
|
@ -520,9 +598,10 @@ pub fn handle_message_edit(cv: &CommunicationValue) -> CommunicationValue {
|
|||
return error_response(cv, CommunicationType::ErrorInvalidData);
|
||||
};
|
||||
|
||||
match chat_files::edit_message(
|
||||
match chat_files::edit_message_for_principal(
|
||||
mutation.sender_id,
|
||||
mutation.partner_id,
|
||||
mutation.partner_principal,
|
||||
mutation.send_time,
|
||||
mutation.sender_id,
|
||||
content,
|
||||
|
|
@ -545,19 +624,23 @@ pub fn handle_message_reaction(cv: &CommunicationValue, add: bool) -> Communicat
|
|||
}
|
||||
|
||||
let result = if add {
|
||||
chat_files::add_reaction(
|
||||
chat_files::add_reaction_for_principal(
|
||||
mutation.sender_id,
|
||||
mutation.partner_id,
|
||||
mutation.partner_principal,
|
||||
mutation.send_time,
|
||||
mutation.sender_id,
|
||||
mutation.sender_principal,
|
||||
reaction,
|
||||
)
|
||||
} else {
|
||||
chat_files::remove_reaction(
|
||||
chat_files::remove_reaction_for_principal(
|
||||
mutation.sender_id,
|
||||
mutation.partner_id,
|
||||
mutation.partner_principal,
|
||||
mutation.send_time,
|
||||
mutation.sender_id,
|
||||
mutation.sender_principal,
|
||||
reaction,
|
||||
)
|
||||
};
|
||||
|
|
@ -577,7 +660,12 @@ pub fn handle_message_delete(cv: &CommunicationValue) -> CommunicationValue {
|
|||
Err(response) => return response,
|
||||
};
|
||||
|
||||
match chat_files::delete_message(mutation.sender_id, mutation.partner_id, mutation.send_time) {
|
||||
match chat_files::delete_message_for_principal(
|
||||
mutation.sender_id,
|
||||
mutation.partner_id,
|
||||
mutation.partner_principal,
|
||||
mutation.send_time,
|
||||
) {
|
||||
Ok(()) => success_response(cv),
|
||||
Err(_) => error_response(cv, CommunicationType::ErrorNotFound),
|
||||
}
|
||||
|
|
@ -738,18 +826,92 @@ pub fn handle_get_chat_secret(cv: &CommunicationValue) -> CommunicationValue {
|
|||
return error_response(cv, CommunicationType::ErrorInvalidData);
|
||||
};
|
||||
|
||||
let mut participants = match chat_id
|
||||
.split(':')
|
||||
.map(str::parse::<i64>)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
{
|
||||
Ok(participants) => participants,
|
||||
Err(_) => return error_response(cv, CommunicationType::ErrorInvalidData),
|
||||
};
|
||||
if participants.len() != 2
|
||||
|| participants.iter().any(|participant| *participant <= 0)
|
||||
|| participants[0] == participants[1]
|
||||
|| !participants.contains(&sender_id)
|
||||
{
|
||||
return error_response(cv, CommunicationType::ErrorInvalidData);
|
||||
}
|
||||
participants.sort_unstable();
|
||||
if chat_id != format!("{}:{}", participants[0], participants[1]) {
|
||||
return error_response(cv, CommunicationType::ErrorInvalidData);
|
||||
}
|
||||
let partner_id = if participants[0] == sender_id {
|
||||
participants[1]
|
||||
} else {
|
||||
participants[0]
|
||||
};
|
||||
let partner_principal = match chats_util::get_user(sender_id, partner_id) {
|
||||
Ok(Some(contact)) => match contact.principal {
|
||||
Some(principal) => principal,
|
||||
None => return error_response(cv, CommunicationType::ErrorNotFound),
|
||||
},
|
||||
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
|
||||
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
|
||||
};
|
||||
let owner_principal = match iota_storage::identity::SqlitePrincipalStore
|
||||
.principal_for_local_user(iota_identity::LocalUserId(sender_id))
|
||||
{
|
||||
Ok(Some(principal)) => principal,
|
||||
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
|
||||
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
|
||||
};
|
||||
let Some(principal_chat_id) =
|
||||
e2ee_storage::principal_chat_id(owner_principal, partner_principal)
|
||||
else {
|
||||
return error_response(cv, CommunicationType::ErrorInvalidData);
|
||||
};
|
||||
let requested_secret_id = data_string(cv, DataType::SecretId);
|
||||
let legacy_main_secret_id = format!("chat:{chat_id}:main");
|
||||
if e2ee_storage::migrate_chat_secret_namespace(
|
||||
&user_id,
|
||||
&chat_id,
|
||||
&legacy_main_secret_id,
|
||||
&principal_chat_id,
|
||||
&e2ee_storage::principal_secret_id(&principal_chat_id),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return error_response(cv, CommunicationType::ErrorInternal);
|
||||
}
|
||||
let principal_secret_id = requested_secret_id.as_ref().map(|secret_id| {
|
||||
if secret_id == &legacy_main_secret_id {
|
||||
e2ee_storage::principal_secret_id(&principal_chat_id)
|
||||
} else {
|
||||
format!("{principal_chat_id}:{secret_id}")
|
||||
}
|
||||
});
|
||||
|
||||
match e2ee_storage::get_chat_secret(ChatSecretQuery {
|
||||
user_id,
|
||||
chat_id,
|
||||
secret_id: data_string(cv, DataType::SecretId),
|
||||
chat_id: principal_chat_id.clone(),
|
||||
secret_id: principal_secret_id,
|
||||
version: data_i64(cv, DataType::VersionNumber),
|
||||
}) {
|
||||
Ok(Some(record)) => CommunicationValue::new(CommunicationType::ChatSecretResponse)
|
||||
.with_request_id(cv)
|
||||
.with_receiver(sender_wire_id(sender_id))
|
||||
.add_typed_default(DataType::UserId, DataValue::Str(record.user_id))
|
||||
.add_typed_default(DataType::ChatId, DataValue::Str(record.chat_id))
|
||||
.add_typed_default(DataType::SecretId, DataValue::Str(record.secret_id))
|
||||
.add_typed_default(DataType::ChatId, DataValue::Str(chat_id))
|
||||
.add_typed_default(
|
||||
DataType::SecretId,
|
||||
DataValue::Str(requested_secret_id.unwrap_or_else(|| {
|
||||
if record.secret_id == e2ee_storage::principal_secret_id(&principal_chat_id) {
|
||||
legacy_main_secret_id
|
||||
} else {
|
||||
record.secret_id
|
||||
}
|
||||
})),
|
||||
)
|
||||
.add_typed_default(
|
||||
DataType::VersionNumber,
|
||||
DataValue::SignedNumber(record.version as i128),
|
||||
|
|
@ -1030,7 +1192,7 @@ pub fn handle_account_state_request(cv: &CommunicationValue) -> CommunicationVal
|
|||
DataValue::Array(
|
||||
blocked_users
|
||||
.into_iter()
|
||||
.map(|id| DataValue::SignedNumber(id.into()))
|
||||
.map(|blocked| DataValue::SignedNumber(blocked.blocked_user_id.into()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
|
|
@ -1153,7 +1315,7 @@ pub fn handle_message_get(cv: &CommunicationValue) -> CommunicationValue {
|
|||
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
|
||||
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
|
||||
},
|
||||
None => match chat_files::get_message(owner, send_time, None) {
|
||||
None => match chat_files::get_message(owner, send_time, None, None) {
|
||||
Ok(Some(message)) => (message, None),
|
||||
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
|
||||
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
|
||||
|
|
@ -1262,7 +1424,20 @@ pub fn handle_read_notification(cv: &CommunicationValue) -> NotificationMutation
|
|||
};
|
||||
};
|
||||
|
||||
match chats_util::read_notifications(owner, partner_id, through) {
|
||||
let Ok(Some(contact)) = chats_util::get_user(owner, partner_id) else {
|
||||
return NotificationMutation {
|
||||
response: error_response(cv, CommunicationType::ErrorNotFound),
|
||||
changed: None,
|
||||
};
|
||||
};
|
||||
let Some(principal) = contact.principal else {
|
||||
return NotificationMutation {
|
||||
response: error_response(cv, CommunicationType::ErrorInternal),
|
||||
changed: None,
|
||||
};
|
||||
};
|
||||
|
||||
match chats_util::read_notifications(owner, principal, through) {
|
||||
Ok(Some(contact)) => NotificationMutation {
|
||||
response: notification_response(
|
||||
CommunicationType::ReadNotification,
|
||||
|
|
@ -2161,7 +2336,19 @@ pub fn handle_user_block(cv: &CommunicationValue) -> PolicyMutation {
|
|||
changed: None,
|
||||
};
|
||||
};
|
||||
match blocked_users::block(user_id, blocked_user_id) {
|
||||
let Ok(Some(contact)) = chats_util::get_user(user_id, blocked_user_id) else {
|
||||
return PolicyMutation {
|
||||
response: error_response(cv, CommunicationType::ErrorNotFound),
|
||||
changed: None,
|
||||
};
|
||||
};
|
||||
let Some(blocked_principal) = contact.principal else {
|
||||
return PolicyMutation {
|
||||
response: error_response(cv, CommunicationType::ErrorInternal),
|
||||
changed: None,
|
||||
};
|
||||
};
|
||||
match blocked_users::block(user_id, blocked_principal) {
|
||||
Ok(record) => PolicyMutation {
|
||||
response: CommunicationValue::new(CommunicationType::UserBlock)
|
||||
.with_request_id(cv)
|
||||
|
|
@ -2209,7 +2396,19 @@ pub fn handle_user_unblock(cv: &CommunicationValue) -> PolicyMutation {
|
|||
changed: None,
|
||||
};
|
||||
};
|
||||
match blocked_users::unblock(user_id, blocked_user_id) {
|
||||
let Ok(Some(contact)) = chats_util::get_user(user_id, blocked_user_id) else {
|
||||
return PolicyMutation {
|
||||
response: error_response(cv, CommunicationType::ErrorNotFound),
|
||||
changed: None,
|
||||
};
|
||||
};
|
||||
let Some(blocked_principal) = contact.principal else {
|
||||
return PolicyMutation {
|
||||
response: error_response(cv, CommunicationType::ErrorInternal),
|
||||
changed: None,
|
||||
};
|
||||
};
|
||||
match blocked_users::unblock(user_id, blocked_principal) {
|
||||
Ok(mutation) => PolicyMutation {
|
||||
response: CommunicationValue::new(CommunicationType::UserUnblock)
|
||||
.with_request_id(cv)
|
||||
|
|
@ -2253,7 +2452,7 @@ pub fn handle_blocked_users_get(cv: &CommunicationValue) -> CommunicationValue {
|
|||
DataValue::Array(
|
||||
users
|
||||
.into_iter()
|
||||
.map(|id| DataValue::SignedNumber(id.into()))
|
||||
.map(|blocked| DataValue::SignedNumber(blocked.blocked_user_id.into()))
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
|
|
@ -2422,7 +2621,15 @@ pub fn handle_user_block_check(cv: &CommunicationValue) -> CommunicationValue {
|
|||
let Some(receiver_id) = data_i64(cv, DataType::ReceiverId).filter(|id| *id > 0) else {
|
||||
return error_response(cv, CommunicationType::ErrorInvalidData);
|
||||
};
|
||||
match blocked_users::is_blocked(receiver_id, sender_id) {
|
||||
let sender_principal = match chats_util::get_user(receiver_id, sender_id) {
|
||||
Ok(Some(contact)) => match contact.principal {
|
||||
Some(principal) => principal,
|
||||
None => return error_response(cv, CommunicationType::ErrorNotFound),
|
||||
},
|
||||
Ok(None) => return error_response(cv, CommunicationType::ErrorNotFound),
|
||||
Err(_) => return error_response(cv, CommunicationType::ErrorInternal),
|
||||
};
|
||||
match blocked_users::is_principal_blocked(receiver_id, sender_principal) {
|
||||
Ok(blocked) => CommunicationValue::new(CommunicationType::UserBlockCheck)
|
||||
.with_request_id(cv)
|
||||
.add_typed_default(DataType::IsBlocked, DataValue::Bool(blocked)),
|
||||
|
|
@ -2439,9 +2646,11 @@ mod stored_message_tests {
|
|||
#[test]
|
||||
fn relay_message_value_has_a_single_sender_id() {
|
||||
let message = StoredMessage {
|
||||
external_principal: None,
|
||||
id: 1,
|
||||
external_user: 9,
|
||||
relay_signer_id: Some(9),
|
||||
relay_signer_principal: None,
|
||||
relay_message_id: Some("relay-1".to_string()),
|
||||
message_time: 2,
|
||||
authored_at: None,
|
||||
|
|
@ -2549,3 +2758,47 @@ mod synced_settings_tests {
|
|||
assert!(response.is_err());
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RelayApplicationContext {
|
||||
pub signer: iota_identity::PrincipalHandle,
|
||||
pub recipient: iota_identity::PrincipalHandle,
|
||||
pub signer_principal: iota_identity::PrincipalId,
|
||||
pub recipient_principal: iota_identity::PrincipalId,
|
||||
pub hosted_sender: Option<iota_identity::LocalUserId>,
|
||||
pub hosted_recipient: Option<iota_identity::LocalUserId>,
|
||||
pub legacy: Option<LegacyRelayApplicationData>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LegacyRelayApplicationData {
|
||||
pub signer_id: u64,
|
||||
pub recipient_id: u64,
|
||||
}
|
||||
|
||||
pub fn apply_relay_application_content(
|
||||
application: &RelayApplicationContext,
|
||||
context: &VerifiedRelayContext,
|
||||
content: &VerifiedRelayContent,
|
||||
accepted_at: i64,
|
||||
) -> Result<(), String> {
|
||||
if application.signer_principal.user_id != context.signer_id
|
||||
|| application.recipient_principal.user_id != context.final_recipient_id
|
||||
{
|
||||
return Err("Relay application identity does not match verified content".into());
|
||||
}
|
||||
let (storage_owner, sent_by_self) =
|
||||
match (application.hosted_sender, application.hosted_recipient) {
|
||||
(Some(sender), _) => (sender.0, true),
|
||||
(None, Some(recipient)) => (recipient.0, false),
|
||||
(None, None) => return Err("Relay application has no hosted principal".into()),
|
||||
};
|
||||
apply_verified_relay_content(
|
||||
context,
|
||||
content,
|
||||
application.signer,
|
||||
application.recipient,
|
||||
accepted_at,
|
||||
storage_owner,
|
||||
sent_by_self,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
1875
iota-connection/src/relay_service.rs
Normal file
1875
iota-connection/src/relay_service.rs
Normal file
File diff suppressed because it is too large
Load diff
116
iota-connection/tests/pending_restart.rs
Normal file
116
iota-connection/tests/pending_restart.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use async_trait::async_trait;
|
||||
use iota_connection::connection_handler::{PeerRouter, RouteDestination, RouteError, RouteOutcome};
|
||||
use iota_connection::relay_service::PendingRelayDispatcher;
|
||||
use iota_identity::{IotaNodeId, PrincipalHandle};
|
||||
use iota_storage::util::relay_queue::{self, RelayIdentity, RelayTarget};
|
||||
use mtp::codec::{CommunicationType, CommunicationValue};
|
||||
use mtp::crypto::Keyring;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
struct AcceptingRouter;
|
||||
|
||||
#[async_trait]
|
||||
impl PeerRouter for AcceptingRouter {
|
||||
async fn route(
|
||||
&self,
|
||||
destination: &RouteDestination,
|
||||
_: CommunicationValue,
|
||||
) -> Result<RouteOutcome, RouteError> {
|
||||
assert!(matches!(destination, RouteDestination::Iota(_)));
|
||||
Ok(RouteOutcome::Accepted {
|
||||
relay_message_id: "restart-relay".into(),
|
||||
destination_accepted_at: 10,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize(storage: &Path) {
|
||||
iota_util::file_util::configure_storage_directory(storage.to_owned());
|
||||
iota_storage::util::db::initialize_database().unwrap();
|
||||
}
|
||||
|
||||
fn seed(storage: &Path) {
|
||||
initialize(storage);
|
||||
let (signer, recipient) = iota_storage::util::db::with_db(|connection| {
|
||||
connection.execute(
|
||||
"INSERT INTO principals (authority_kind, authority_id, remote_user_id, descriptor_revision, last_resolved_at) VALUES ('iota', 'iota:first', 1, 1, 1)",
|
||||
[],
|
||||
)?;
|
||||
let signer = connection.last_insert_rowid();
|
||||
connection.execute(
|
||||
"INSERT INTO principals (authority_kind, authority_id, remote_user_id, descriptor_revision, last_resolved_at) VALUES ('iota', 'iota:second', 1, 1, 1)",
|
||||
[],
|
||||
)?;
|
||||
Ok((signer, connection.last_insert_rowid()))
|
||||
})
|
||||
.unwrap();
|
||||
let frame = CommunicationValue::new(CommunicationType::Relay).with_id(7);
|
||||
let version = frame.type_map().unwrap().version.to_string();
|
||||
let bytes = frame.to_bytes().unwrap();
|
||||
relay_queue::enqueue(
|
||||
RelayTarget::Iota(
|
||||
IotaNodeId::from_public_keys(&Keyring::generate().public_key_bundle()).unwrap(),
|
||||
),
|
||||
&RelayIdentity {
|
||||
signer: PrincipalHandle(signer),
|
||||
recipient: PrincipalHandle(recipient),
|
||||
message_id: "restart-relay".into(),
|
||||
legacy_signer_id: None,
|
||||
legacy_recipient_id: None,
|
||||
},
|
||||
&bytes,
|
||||
0,
|
||||
7,
|
||||
&version,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(relay_queue::list_active(10).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
async fn deliver(storage: &Path) {
|
||||
initialize(storage);
|
||||
let dispatcher = PendingRelayDispatcher::new(std::sync::Arc::new(AcceptingRouter));
|
||||
assert_eq!(dispatcher.dispatch_ready(10).await.unwrap(), 1);
|
||||
assert_eq!(dispatcher.dispatch_ready(10).await.unwrap(), 0);
|
||||
assert!(relay_queue::list(10).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pending_relay_survives_process_restart_and_delivers_once() {
|
||||
let storage =
|
||||
std::env::var_os("TENSAMIN_PENDING_RESTART_STORAGE").map(std::path::PathBuf::from);
|
||||
match std::env::var("TENSAMIN_PENDING_RESTART_PHASE").as_deref() {
|
||||
Ok("seed") => {
|
||||
seed(storage.as_deref().unwrap());
|
||||
return;
|
||||
}
|
||||
Ok("deliver") => {
|
||||
deliver(storage.as_deref().unwrap()).await;
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let storage = std::env::temp_dir().join(format!(
|
||||
"tensamin-pending-restart-{}-{}",
|
||||
std::process::id(),
|
||||
iota_storage::util::sync::now_millis()
|
||||
));
|
||||
std::fs::create_dir(&storage).unwrap();
|
||||
let executable = std::env::current_exe().unwrap();
|
||||
for phase in ["seed", "deliver"] {
|
||||
let status = Command::new(&executable)
|
||||
.args([
|
||||
"--exact",
|
||||
"pending_relay_survives_process_restart_and_delivers_once",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("TENSAMIN_PENDING_RESTART_PHASE", phase)
|
||||
.env("TENSAMIN_PENDING_RESTART_STORAGE", &storage)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success(), "{phase} child process failed");
|
||||
}
|
||||
std::fs::remove_dir_all(&storage).unwrap();
|
||||
}
|
||||
Loading…
Reference in a new issue