Squashed 'components/mycelium/' content from commit afb32e0

git-subtree-dir: components/mycelium
git-subtree-split: afb32e0cdb2d4cdd17f22a5693278068d061f08c
This commit is contained in:
2025-08-16 21:12:34 +02:00
commit 10025f9fa5
132 changed files with 50951 additions and 0 deletions

37
mycelium-api/Cargo.toml Normal file
View File

@@ -0,0 +1,37 @@
[package]
name = "mycelium-api"
version = "0.6.1"
edition = "2021"
license-file = "../LICENSE"
readme = "../README.md"
[features]
message = ["mycelium/message"]
[dependencies]
axum = { version = "0.8.4", default-features = false, features = [
"http1",
"http2",
"json",
"query",
"tokio",
] }
base64 = "0.22.1"
jsonrpsee = { version = "0.25.1", features = [
"server",
"macros",
"jsonrpsee-types",
] }
serde_json = "1.0.140"
tracing = "0.1.41"
tokio = { version = "1.46.1", default-features = false, features = [
"net",
"rt",
] }
mycelium = { path = "../mycelium" }
mycelium-metrics = { path = "../mycelium-metrics", features = ["prometheus"] }
serde = { version = "1.0.219", features = ["derive"] }
async-trait = "0.1.88"
[dev-dependencies]
serde_json = "1.0.140"

492
mycelium-api/src/lib.rs Normal file
View File

@@ -0,0 +1,492 @@
use core::fmt;
use std::{net::IpAddr, net::SocketAddr, str::FromStr, sync::Arc};
use axum::{
extract::{Path, State},
http::StatusCode,
routing::{delete, get},
Json, Router,
};
use serde::{de, Deserialize, Deserializer, Serialize};
use tokio::{sync::Mutex, time::Instant};
use tracing::{debug, error};
use mycelium::{
crypto::PublicKey,
endpoint::Endpoint,
metrics::Metrics,
peer_manager::{PeerExists, PeerNotFound, PeerStats},
};
const INFINITE_STR: &str = "infinite";
#[cfg(feature = "message")]
mod message;
#[cfg(feature = "message")]
pub use message::{MessageDestination, MessageReceiveInfo, MessageSendInfo, PushMessageResponse};
pub use rpc::JsonRpc;
// JSON-RPC API implementation
pub mod rpc;
/// Http API server handle. The server is spawned in a background task. If this handle is dropped,
/// the server is terminated.
pub struct Http {
/// Channel to send cancellation to the http api server. We just keep a reference to it since
/// dropping it will also cancel the receiver and thus the server.
_cancel_tx: tokio::sync::oneshot::Sender<()>,
}
#[derive(Clone)]
/// Shared state accessible in HTTP endpoint handlers.
pub struct ServerState<M> {
/// Access to the (`node`)(mycelium::Node) state.
pub node: Arc<Mutex<mycelium::Node<M>>>,
}
impl Http {
/// Spawns a new HTTP API server on the provided listening address.
pub fn spawn<M>(node: Arc<Mutex<mycelium::Node<M>>>, listen_addr: SocketAddr) -> Self
where
M: Metrics + Clone + Send + Sync + 'static,
{
let server_state = ServerState { node };
let admin_routes = Router::new()
.route("/admin", get(get_info))
.route("/admin/peers", get(get_peers).post(add_peer))
.route("/admin/peers/{endpoint}", delete(delete_peer))
.route("/admin/routes/selected", get(get_selected_routes))
.route("/admin/routes/fallback", get(get_fallback_routes))
.route("/admin/routes/queried", get(get_queried_routes))
.route("/admin/routes/no_route", get(get_no_route_entries))
.route("/pubkey/{ip}", get(get_pubk_from_ip))
.with_state(server_state.clone());
let app = Router::new().nest("/api/v1", admin_routes);
#[cfg(feature = "message")]
let app = app.nest("/api/v1", message::message_router_v1(server_state));
let (_cancel_tx, cancel_rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
let listener = match tokio::net::TcpListener::bind(listen_addr).await {
Ok(listener) => listener,
Err(e) => {
error!(err=%e, "Failed to bind listener for Http Api server");
error!("API disabled");
return;
}
};
let server =
axum::serve(listener, app.into_make_service()).with_graceful_shutdown(async {
cancel_rx.await.ok();
});
if let Err(e) = server.await {
error!(err=%e, "Http API server error");
}
});
Http { _cancel_tx }
}
}
/// Get the stats of the current known peers
async fn get_peers<M>(State(state): State<ServerState<M>>) -> Json<Vec<PeerStats>>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Fetching peer stats");
Json(state.node.lock().await.peer_info())
}
/// Payload of an add_peer request
#[derive(Deserialize, Serialize)]
pub struct AddPeer {
/// The endpoint used to connect to the peer
pub endpoint: String,
}
/// Add a new peer to the system
async fn add_peer<M>(
State(state): State<ServerState<M>>,
Json(payload): Json<AddPeer>,
) -> Result<StatusCode, (StatusCode, String)>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!(
peer.endpoint = payload.endpoint,
"Attempting to add peer to the system"
);
let endpoint = match Endpoint::from_str(&payload.endpoint) {
Ok(endpoint) => endpoint,
Err(e) => return Err((StatusCode::BAD_REQUEST, e.to_string())),
};
match state.node.lock().await.add_peer(endpoint) {
Ok(()) => Ok(StatusCode::NO_CONTENT),
Err(PeerExists) => Err((
StatusCode::CONFLICT,
"A peer identified by that endpoint already exists".to_string(),
)),
}
}
/// remove an existing peer from the system
async fn delete_peer<M>(
State(state): State<ServerState<M>>,
Path(endpoint): Path<String>,
) -> Result<StatusCode, (StatusCode, String)>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!(peer.endpoint=%endpoint, "Attempting to remove peer from the system");
let endpoint = match Endpoint::from_str(&endpoint) {
Ok(endpoint) => endpoint,
Err(e) => return Err((StatusCode::BAD_REQUEST, e.to_string())),
};
match state.node.lock().await.remove_peer(endpoint) {
Ok(()) => Ok(StatusCode::NO_CONTENT),
Err(PeerNotFound) => Err((
StatusCode::NOT_FOUND,
"A peer identified by that endpoint does not exist".to_string(),
)),
}
}
/// Alias to a [`Metric`](crate::metric::Metric) for serialization in the API.
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
pub enum Metric {
/// Finite metric
Value(u16),
/// Infinite metric
Infinite,
}
/// Info about a route. This uses base types only to avoid having to introduce too many Serialize
/// bounds in the core types.
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord)]
#[serde(rename_all = "camelCase")]
pub struct Route {
/// We convert the [`subnet`](Subnet) to a string to avoid introducing a bound on the actual
/// type.
pub subnet: String,
/// Next hop of the route, in the underlay.
pub next_hop: String,
/// Computed metric of the route.
pub metric: Metric,
/// Sequence number of the route.
pub seqno: u16,
}
/// List all currently selected routes.
async fn get_selected_routes<M>(State(state): State<ServerState<M>>) -> Json<Vec<Route>>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Loading selected routes");
let routes = state
.node
.lock()
.await
.selected_routes()
.into_iter()
.map(|sr| Route {
subnet: sr.source().subnet().to_string(),
next_hop: sr.neighbour().connection_identifier().clone(),
metric: if sr.metric().is_infinite() {
Metric::Infinite
} else {
Metric::Value(sr.metric().into())
},
seqno: sr.seqno().into(),
})
.collect();
Json(routes)
}
/// List all active fallback routes.
async fn get_fallback_routes<M>(State(state): State<ServerState<M>>) -> Json<Vec<Route>>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Loading fallback routes");
let routes = state
.node
.lock()
.await
.fallback_routes()
.into_iter()
.map(|sr| Route {
subnet: sr.source().subnet().to_string(),
next_hop: sr.neighbour().connection_identifier().clone(),
metric: if sr.metric().is_infinite() {
Metric::Infinite
} else {
Metric::Value(sr.metric().into())
},
seqno: sr.seqno().into(),
})
.collect();
Json(routes)
}
/// Info about a queried subnet. This uses base types only to avoid having to introduce too
/// many Serialize bounds in the core types.
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord)]
#[serde(rename_all = "camelCase")]
pub struct QueriedSubnet {
/// We convert the [`subnet`](Subnet) to a string to avoid introducing a bound on the actual
/// type.
pub subnet: String,
/// The amount of time left before the query expires.
pub expiration: String,
}
async fn get_queried_routes<M>(State(state): State<ServerState<M>>) -> Json<Vec<QueriedSubnet>>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Loading queried subnets");
let queries = state
.node
.lock()
.await
.queried_subnets()
.into_iter()
.map(|qs| QueriedSubnet {
subnet: qs.subnet().to_string(),
expiration: qs
.query_expires()
.duration_since(Instant::now())
.as_secs()
.to_string(),
})
.collect();
Json(queries)
}
/// Info about a subnet with no route. This uses base types only to avoid having to introduce too
/// many Serialize bounds in the core types.
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord)]
#[serde(rename_all = "camelCase")]
pub struct NoRouteSubnet {
/// We convert the [`subnet`](Subnet) to a string to avoid introducing a bound on the actual
/// type.
pub subnet: String,
/// The amount of time left before the query expires.
pub expiration: String,
}
async fn get_no_route_entries<M>(State(state): State<ServerState<M>>) -> Json<Vec<NoRouteSubnet>>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Loading queried subnets");
let queries = state
.node
.lock()
.await
.no_route_entries()
.into_iter()
.map(|nrs| NoRouteSubnet {
subnet: nrs.subnet().to_string(),
expiration: nrs
.entry_expires()
.duration_since(Instant::now())
.as_secs()
.to_string(),
})
.collect();
Json(queries)
}
/// General info about a node.
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Info {
/// The overlay subnet in use by the node.
pub node_subnet: String,
/// The public key of the node
pub node_pubkey: PublicKey,
}
/// Get general info about the node.
async fn get_info<M>(State(state): State<ServerState<M>>) -> Json<Info>
where
M: Metrics + Clone + Send + Sync + 'static,
{
let info = state.node.lock().await.info();
Json(Info {
node_subnet: info.node_subnet.to_string(),
node_pubkey: info.node_pubkey,
})
}
/// Public key from a node.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PubKey {
/// The public key from the node
pub public_key: PublicKey,
}
/// Get public key from IP.
async fn get_pubk_from_ip<M>(
State(state): State<ServerState<M>>,
Path(ip): Path<IpAddr>,
) -> Result<Json<PubKey>, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
match state.node.lock().await.get_pubkey_from_ip(ip) {
Some(pubkey) => Ok(Json(PubKey { public_key: pubkey })),
None => Err(StatusCode::NOT_FOUND),
}
}
impl Serialize for Metric {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Infinite => serializer.serialize_str(INFINITE_STR),
Self::Value(v) => serializer.serialize_u16(*v),
}
}
}
impl<'de> Deserialize<'de> for Metric {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct MetricVisitor;
impl serde::de::Visitor<'_> for MetricVisitor {
type Value = Metric;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or a u16")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match value {
INFINITE_STR => Ok(Metric::Infinite),
_ => Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(value),
&format!("expected '{INFINITE_STR}'").as_str(),
)),
}
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
if value <= u16::MAX as u64 {
Ok(Metric::Value(value as u16))
} else {
Err(E::invalid_value(
de::Unexpected::Unsigned(value),
&"expected a non-negative integer within the range of u16",
))
}
}
}
deserializer.deserialize_any(MetricVisitor)
}
}
impl fmt::Display for Metric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Value(val) => write!(f, "{val}"),
Self::Infinite => write!(f, "{INFINITE_STR}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn finite_metric_serialization() {
let metric = super::Metric::Value(10);
let s = serde_json::to_string(&metric).expect("can encode finite metric");
assert_eq!("10", s);
}
#[test]
fn infinite_metric_serialization() {
let metric = super::Metric::Infinite;
let s = serde_json::to_string(&metric).expect("can encode infinite metric");
assert_eq!(format!("\"{INFINITE_STR}\""), s);
}
#[test]
fn test_deserialize_metric() {
// Test deserialization of a Metric::Value
let json_value = json!(20);
let metric: Metric = serde_json::from_value(json_value).unwrap();
assert_eq!(metric, Metric::Value(20));
// Test deserialization of a Metric::Infinite
let json_infinite = json!(INFINITE_STR);
let metric: Metric = serde_json::from_value(json_infinite).unwrap();
assert_eq!(metric, Metric::Infinite);
// Test deserialization of an invalid metric
let json_invalid = json!("invalid");
let result: Result<Metric, _> = serde_json::from_value(json_invalid);
assert!(result.is_err());
}
#[test]
fn test_deserialize_route() {
let json_data = r#"
[
{"subnet":"406:1d77:2438:aa7c::/64","nextHop":"TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651","metric":20,"seqno":0},
{"subnet":"407:8458:dbf5:4ed7::/64","nextHop":"TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651","metric":174,"seqno":0},
{"subnet":"408:7ba3:3a4d:808a::/64","nextHop":"TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651","metric":"infinite","seqno":0}
]
"#;
let routes: Vec<Route> = serde_json::from_str(json_data).unwrap();
assert_eq!(routes[0], Route {
subnet: "406:1d77:2438:aa7c::/64".to_string(),
next_hop: "TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651".to_string(),
metric: Metric::Value(20),
seqno: 0
});
assert_eq!(routes[1], Route {
subnet: "407:8458:dbf5:4ed7::/64".to_string(),
next_hop: "TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651".to_string(),
metric: Metric::Value(174),
seqno: 0
});
assert_eq!(routes[2], Route {
subnet: "408:7ba3:3a4d:808a::/64".to_string(),
next_hop: "TCP [2a02:1811:d584:7400:c503:ff39:de03:9e44]:45694 <-> [2a01:4f8:212:fa6::2]:9651".to_string(),
metric: Metric::Infinite,
seqno: 0
});
}
}

652
mycelium-api/src/message.rs Normal file
View File

@@ -0,0 +1,652 @@
use std::{net::IpAddr, ops::Deref, time::Duration};
use axum::{
extract::{Path, Query, State},
http::StatusCode,
routing::{delete, get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use tracing::debug;
use mycelium::{
crypto::PublicKey,
message::{MessageId, MessageInfo},
metrics::Metrics,
subnet::Subnet,
};
use std::path::PathBuf;
use super::ServerState;
/// Default amount of time to try and send a message if it is not explicitly specified.
const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5);
/// Return a router which has message endpoints and their handlers mounted.
pub fn message_router_v1<M>(server_state: ServerState<M>) -> Router
where
M: Metrics + Clone + Send + Sync + 'static,
{
Router::new()
.route("/messages", get(get_message).post(push_message))
.route("/messages/status/{id}", get(message_status))
.route("/messages/reply/{id}", post(reply_message))
// Topic configuration endpoints
.route(
"/messages/topics/default",
get(get_default_topic_action).put(set_default_topic_action),
)
.route("/messages/topics", get(get_topics).post(add_topic))
.route("/messages/topics/{topic}", delete(remove_topic))
.route(
"/messages/topics/{topic}/sources",
get(get_topic_sources).post(add_topic_source),
)
.route(
"/messages/topics/{topic}/sources/{subnet}",
delete(remove_topic_source),
)
.route(
"/messages/topics/{topic}/forward",
get(get_topic_forward_socket)
.put(set_topic_forward_socket)
.delete(remove_topic_forward_socket),
)
.with_state(server_state)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageSendInfo {
pub dst: MessageDestination,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "base64::optional_binary")]
pub topic: Option<Vec<u8>>,
#[serde(with = "base64::binary")]
pub payload: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum MessageDestination {
Ip(IpAddr),
Pk(PublicKey),
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageReceiveInfo {
pub id: MessageId,
pub src_ip: IpAddr,
pub src_pk: PublicKey,
pub dst_ip: IpAddr,
pub dst_pk: PublicKey,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "base64::optional_binary")]
pub topic: Option<Vec<u8>>,
#[serde(with = "base64::binary")]
pub payload: Vec<u8>,
}
impl MessageDestination {
/// Get the IP address of the destination.
fn ip(self) -> IpAddr {
match self {
MessageDestination::Ip(ip) => ip,
MessageDestination::Pk(pk) => IpAddr::V6(pk.address()),
}
}
}
#[derive(Deserialize)]
struct GetMessageQuery {
peek: Option<bool>,
timeout: Option<u64>,
/// Optional filter for start of the message, base64 encoded.
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(with = "base64::optional_binary")]
topic: Option<Vec<u8>>,
}
impl GetMessageQuery {
/// Did the query indicate we should peek the message instead of pop?
fn peek(&self) -> bool {
matches!(self.peek, Some(true))
}
/// Amount of seconds to hold and try and get values.
fn timeout_secs(&self) -> u64 {
self.timeout.unwrap_or(0)
}
}
async fn get_message<M>(
State(state): State<ServerState<M>>,
Query(query): Query<GetMessageQuery>,
) -> Result<Json<MessageReceiveInfo>, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!(
"Attempt to get message, peek {}, timeout {} seconds",
query.peek(),
query.timeout_secs()
);
// A timeout of 0 seconds essentially means get a message if there is one, and return
// immediatly if there isn't. This is the result of the implementation of Timeout, which does a
// poll of the internal future first, before polling the delay.
tokio::time::timeout(
Duration::from_secs(query.timeout_secs()),
state
.node
.lock()
.await
.get_message(!query.peek(), query.topic),
)
.await
.or(Err(StatusCode::NO_CONTENT))
.map(|m| {
Json(MessageReceiveInfo {
id: m.id,
src_ip: m.src_ip,
src_pk: m.src_pk,
dst_ip: m.dst_ip,
dst_pk: m.dst_pk,
topic: if m.topic.is_empty() {
None
} else {
Some(m.topic)
},
payload: m.data,
})
})
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageIdReply {
pub id: MessageId,
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum PushMessageResponse {
Reply(MessageReceiveInfo),
Id(MessageIdReply),
}
#[derive(Clone, Deserialize)]
struct PushMessageQuery {
reply_timeout: Option<u64>,
}
impl PushMessageQuery {
/// The user requested to wait for the reply or not.
fn await_reply(&self) -> bool {
self.reply_timeout.is_some()
}
/// Amount of seconds to wait for the reply.
fn timeout(&self) -> u64 {
self.reply_timeout.unwrap_or(0)
}
}
async fn push_message<M>(
State(state): State<ServerState<M>>,
Query(query): Query<PushMessageQuery>,
Json(message_info): Json<MessageSendInfo>,
) -> Result<(StatusCode, Json<PushMessageResponse>), StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
let dst = message_info.dst.ip();
debug!(
message.dst=%dst,
message.len=message_info.payload.len(),
"Pushing new message to stack",
);
let (id, sub) = match state.node.lock().await.push_message(
dst,
message_info.payload,
message_info.topic,
DEFAULT_MESSAGE_TRY_DURATION,
query.await_reply(),
) {
Ok((id, sub)) => (id, sub),
Err(_) => {
return Err(StatusCode::BAD_REQUEST);
}
};
if !query.await_reply() {
// If we don't wait for the reply just return here.
return Ok((
StatusCode::CREATED,
Json(PushMessageResponse::Id(MessageIdReply { id })),
));
}
let mut sub = sub.unwrap();
tokio::select! {
sub_res = sub.changed() => {
match sub_res {
Ok(_) => {
if let Some(m) = sub.borrow().deref() {
Ok((StatusCode::OK, Json(PushMessageResponse::Reply(MessageReceiveInfo {
id: m.id,
src_ip: m.src_ip,
src_pk: m.src_pk,
dst_ip: m.dst_ip,
dst_pk: m.dst_pk,
topic: if m.topic.is_empty() { None } else { Some(m.topic.clone()) },
payload: m.data.clone(),
}))))
} else {
// This happens if a none value is send, which should not happen.
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
Err(_) => {
// This happens if the sender drops, which should not happen.
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
},
_ = tokio::time::sleep(Duration::from_secs(query.timeout())) => {
// Timeout expired while waiting for reply
Ok((StatusCode::REQUEST_TIMEOUT, Json(PushMessageResponse::Id(MessageIdReply { id }))))
}
}
}
async fn reply_message<M>(
State(state): State<ServerState<M>>,
Path(id): Path<MessageId>,
Json(message_info): Json<MessageSendInfo>,
) -> StatusCode
where
M: Metrics + Clone + Send + Sync + 'static,
{
let dst = message_info.dst.ip();
debug!(
message.id=id.as_hex(),
message.dst=%dst,
message.len=message_info.payload.len(),
"Pushing new reply to message stack",
);
state.node.lock().await.reply_message(
id,
dst,
message_info.payload,
DEFAULT_MESSAGE_TRY_DURATION,
);
StatusCode::NO_CONTENT
}
async fn message_status<M>(
State(state): State<ServerState<M>>,
Path(id): Path<MessageId>,
) -> Result<Json<MessageInfo>, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!(message.id=%id.as_hex(), "Fetching message status");
state
.node
.lock()
.await
.message_status(id)
.ok_or(StatusCode::NOT_FOUND)
.map(Json)
}
/// Module to implement base64 decoding and encoding
/// Sourced from https://users.rust-lang.org/t/serialize-a-vec-u8-to-json-as-base64/57781, with some
/// addaptions to work with the new version of the base64 crate
mod base64 {
use base64::engine::{GeneralPurpose, GeneralPurposeConfig};
use base64::{alphabet, Engine};
const B64ENGINE: GeneralPurpose = base64::engine::general_purpose::GeneralPurpose::new(
&alphabet::STANDARD,
GeneralPurposeConfig::new(),
);
pub fn encode(input: &[u8]) -> String {
B64ENGINE.encode(input)
}
pub fn decode(input: &[u8]) -> Result<Vec<u8>, base64::DecodeError> {
B64ENGINE.decode(input)
}
pub mod binary {
use super::B64ENGINE;
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
let base64 = B64ENGINE.encode(v);
String::serialize(&base64, s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let base64 = String::deserialize(d)?;
B64ENGINE
.decode(base64.as_bytes())
.map_err(serde::de::Error::custom)
}
}
pub mod optional_binary {
use super::B64ENGINE;
use base64::Engine;
use serde::{Deserialize, Serialize};
use serde::{Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &Option<Vec<u8>>, s: S) -> Result<S::Ok, S::Error> {
if let Some(v) = v {
let base64 = B64ENGINE.encode(v);
String::serialize(&base64, s)
} else {
<Option<String>>::serialize(&None, s)
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Vec<u8>>, D::Error> {
if let Some(base64) = <Option<String>>::deserialize(d)? {
B64ENGINE
.decode(base64.as_bytes())
.map_err(serde::de::Error::custom)
.map(Option::Some)
} else {
Ok(None)
}
}
}
}
// Topic configuration API
/// Response for the default topic action
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DefaultTopicActionResponse {
accept: bool,
}
/// Request to set the default topic action
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DefaultTopicActionRequest {
accept: bool,
}
/// Request to add a source to a topic whitelist
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TopicSourceRequest {
subnet: String,
}
/// Request to set a forward socket for a topic
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TopicForwardSocketRequest {
socket_path: String,
}
/// Get the default topic action (accept or reject)
async fn get_default_topic_action<M>(
State(state): State<ServerState<M>>,
) -> Json<DefaultTopicActionResponse>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Getting default topic action");
let accept = state.node.lock().await.unconfigure_topic_action();
Json(DefaultTopicActionResponse { accept })
}
/// Set the default topic action (accept or reject)
async fn set_default_topic_action<M>(
State(state): State<ServerState<M>>,
Json(request): Json<DefaultTopicActionRequest>,
) -> StatusCode
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!(accept=%request.accept, "Setting default topic action");
state
.node
.lock()
.await
.accept_unconfigured_topic(request.accept);
StatusCode::NO_CONTENT
}
/// Get all whitelisted topics
async fn get_topics<M>(State(state): State<ServerState<M>>) -> Json<Vec<String>>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Getting all whitelisted topics");
let node = state.node.lock().await;
// Get the whitelist from the node
let topics = node.topics();
// Convert to TopicInfo structs
let topics: Vec<String> = topics.iter().map(|topic| base64::encode(topic)).collect();
Json(topics)
}
/// Add a topic to the whitelist
async fn add_topic<M>(
State(state): State<ServerState<M>>,
Json(topic_info): Json<Vec<u8>>,
) -> StatusCode
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Adding topic to whitelist");
state.node.lock().await.add_topic_whitelist(topic_info);
StatusCode::CREATED
}
/// Remove a topic from the whitelist
async fn remove_topic<M>(
State(state): State<ServerState<M>>,
Path(topic): Path<String>,
) -> Result<StatusCode, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Removing topic from whitelist");
// Decode the base64 topic
let topic_bytes = match base64::decode(topic.as_bytes()) {
Ok(bytes) => bytes,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};
state.node.lock().await.remove_topic_whitelist(topic_bytes);
Ok(StatusCode::NO_CONTENT)
}
/// Get all sources for a topic
async fn get_topic_sources<M>(
State(state): State<ServerState<M>>,
Path(topic): Path<String>,
) -> Result<Json<Vec<String>>, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Getting sources for topic");
// Decode the base64 topic
let topic_bytes = match base64::decode(topic.as_bytes()) {
Ok(bytes) => bytes,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};
let node = state.node.lock().await;
// Get the whitelist from the node
let sources = node.topic_allowed_sources(&topic_bytes);
// Find the topic in the whitelist
if let Some(sources) = sources {
let sources = sources.into_iter().map(|s| s.to_string()).collect();
Ok(Json(sources))
} else {
Err(StatusCode::NOT_FOUND)
}
}
/// Add a source to a topic whitelist
async fn add_topic_source<M>(
State(state): State<ServerState<M>>,
Path(topic): Path<String>,
Json(request): Json<TopicSourceRequest>,
) -> Result<StatusCode, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Adding source to topic whitelist");
// Decode the base64 topic
let topic_bytes = match base64::decode(topic.as_bytes()) {
Ok(bytes) => bytes,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};
// Parse the subnet
let subnet = match request.subnet.parse::<Subnet>() {
Ok(subnet) => subnet,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};
state
.node
.lock()
.await
.add_topic_whitelist_src(topic_bytes, subnet);
Ok(StatusCode::CREATED)
}
/// Remove a source from a topic whitelist
async fn remove_topic_source<M>(
State(state): State<ServerState<M>>,
Path((topic, subnet_str)): Path<(String, String)>,
) -> Result<StatusCode, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Removing source from topic whitelist");
// Decode the base64 topic
let topic_bytes = match base64::decode(topic.as_bytes()) {
Ok(bytes) => bytes,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};
// Parse the subnet
let subnet = match subnet_str.parse::<Subnet>() {
Ok(subnet) => subnet,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};
state
.node
.lock()
.await
.remove_topic_whitelist_src(topic_bytes, subnet);
Ok(StatusCode::NO_CONTENT)
}
/// Get the forward socket for a topic
async fn get_topic_forward_socket<M>(
State(state): State<ServerState<M>>,
Path(topic): Path<String>,
) -> Result<Json<Option<String>>, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Getting forward socket for topic");
// Decode the base64 topic
let topic_bytes = match base64::decode(topic.as_bytes()) {
Ok(bytes) => bytes,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};
let node = state.node.lock().await;
let socket_path = node
.get_topic_forward_socket(&topic_bytes)
.map(|p| p.to_string_lossy().to_string());
Ok(Json(socket_path))
}
/// Set the forward socket for a topic
async fn set_topic_forward_socket<M>(
State(state): State<ServerState<M>>,
Path(topic): Path<String>,
Json(request): Json<TopicForwardSocketRequest>,
) -> Result<StatusCode, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Setting forward socket for topic");
// Decode the base64 topic
let topic_bytes = match base64::decode(topic.as_bytes()) {
Ok(bytes) => bytes,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};
let socket_path = PathBuf::from(request.socket_path);
state
.node
.lock()
.await
.set_topic_forward_socket(topic_bytes, socket_path);
Ok(StatusCode::NO_CONTENT)
}
/// Remove the forward socket for a topic
async fn remove_topic_forward_socket<M>(
State(state): State<ServerState<M>>,
Path(topic): Path<String>,
) -> Result<StatusCode, StatusCode>
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!("Removing forward socket for topic");
// Decode the base64 topic
let topic_bytes = match base64::decode(topic.as_bytes()) {
Ok(bytes) => bytes,
Err(_) => return Err(StatusCode::BAD_REQUEST),
};
state
.node
.lock()
.await
.delete_topic_forward_socket(topic_bytes);
Ok(StatusCode::NO_CONTENT)
}

752
mycelium-api/src/rpc.rs Normal file
View File

@@ -0,0 +1,752 @@
//! JSON-RPC API implementation for Mycelium
mod spec;
use std::net::SocketAddr;
#[cfg(feature = "message")]
use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc;
#[cfg(feature = "message")]
use base64::Engine;
use jsonrpsee::core::RpcResult;
use jsonrpsee::proc_macros::rpc;
use jsonrpsee::server::{ServerBuilder, ServerHandle};
use jsonrpsee::types::{ErrorCode, ErrorObject};
#[cfg(feature = "message")]
use mycelium::subnet::Subnet;
#[cfg(feature = "message")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "message")]
use std::path::PathBuf;
use tokio::sync::Mutex;
#[cfg(feature = "message")]
use tokio::time::Duration;
use tracing::debug;
use crate::{Info, Metric, NoRouteSubnet, QueriedSubnet, Route, ServerState};
use mycelium::crypto::PublicKey;
use mycelium::endpoint::Endpoint;
use mycelium::metrics::Metrics;
use mycelium::peer_manager::{PeerExists, PeerNotFound, PeerStats};
use self::spec::OPENRPC_SPEC;
// Topic configuration struct for RPC API
#[cfg(feature = "message")]
#[derive(Clone, Serialize, Deserialize)]
struct TopicInfo {
topic: String,
sources: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
forward_socket: Option<String>,
}
// Define the base RPC API trait using jsonrpsee macros
#[rpc(server)]
pub trait MyceliumApi {
// Admin methods
#[method(name = "getInfo")]
async fn get_info(&self) -> RpcResult<Info>;
#[method(name = "getPublicKeyFromIp")]
async fn get_pubkey_from_ip(&self, ip: String) -> RpcResult<PublicKey>;
// Peer methods
#[method(name = "getPeers")]
async fn get_peers(&self) -> RpcResult<Vec<PeerStats>>;
#[method(name = "addPeer")]
async fn add_peer(&self, endpoint: String) -> RpcResult<bool>;
#[method(name = "deletePeer")]
async fn delete_peer(&self, endpoint: String) -> RpcResult<bool>;
// Route methods
#[method(name = "getSelectedRoutes")]
async fn get_selected_routes(&self) -> RpcResult<Vec<Route>>;
#[method(name = "getFallbackRoutes")]
async fn get_fallback_routes(&self) -> RpcResult<Vec<Route>>;
#[method(name = "getQueriedSubnets")]
async fn get_queried_subnets(&self) -> RpcResult<Vec<QueriedSubnet>>;
#[method(name = "getNoRouteEntries")]
async fn get_no_route_entries(&self) -> RpcResult<Vec<NoRouteSubnet>>;
// OpenRPC discovery
#[method(name = "rpc.discover")]
async fn discover(&self) -> RpcResult<serde_json::Value>;
}
// Define a separate message API trait that is only compiled when the message feature is enabled
#[cfg(feature = "message")]
#[rpc(server)]
pub trait MyceliumMessageApi {
// Message methods
#[method(name = "popMessage")]
async fn pop_message(
&self,
peek: Option<bool>,
timeout: Option<u64>,
topic: Option<String>,
) -> RpcResult<crate::message::MessageReceiveInfo>;
#[method(name = "pushMessage")]
async fn push_message(
&self,
message: crate::message::MessageSendInfo,
reply_timeout: Option<u64>,
) -> RpcResult<crate::message::PushMessageResponse>;
#[method(name = "pushMessageReply")]
async fn push_message_reply(
&self,
id: String,
message: crate::message::MessageSendInfo,
) -> RpcResult<bool>;
#[method(name = "getMessageInfo")]
async fn get_message_info(&self, id: String) -> RpcResult<mycelium::message::MessageInfo>;
// Topic configuration methods
#[method(name = "getDefaultTopicAction")]
async fn get_default_topic_action(&self) -> RpcResult<bool>;
#[method(name = "setDefaultTopicAction")]
async fn set_default_topic_action(&self, accept: bool) -> RpcResult<bool>;
#[method(name = "getTopics")]
async fn get_topics(&self) -> RpcResult<Vec<String>>;
#[method(name = "addTopic")]
async fn add_topic(&self, topic: String) -> RpcResult<bool>;
#[method(name = "removeTopic")]
async fn remove_topic(&self, topic: String) -> RpcResult<bool>;
#[method(name = "getTopicSources")]
async fn get_topic_sources(&self, topic: String) -> RpcResult<Vec<String>>;
#[method(name = "addTopicSource")]
async fn add_topic_source(&self, topic: String, subnet: String) -> RpcResult<bool>;
#[method(name = "removeTopicSource")]
async fn remove_topic_source(&self, topic: String, subnet: String) -> RpcResult<bool>;
#[method(name = "getTopicForwardSocket")]
async fn get_topic_forward_socket(&self, topic: String) -> RpcResult<Option<String>>;
#[method(name = "setTopicForwardSocket")]
async fn set_topic_forward_socket(&self, topic: String, socket_path: String)
-> RpcResult<bool>;
#[method(name = "removeTopicForwardSocket")]
async fn remove_topic_forward_socket(&self, topic: String) -> RpcResult<bool>;
}
// Implement the API trait
#[derive(Clone)]
struct RPCApi<M> {
state: Arc<ServerState<M>>,
}
// Implement the base API trait
#[async_trait::async_trait]
impl<M> MyceliumApiServer for RPCApi<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
async fn get_info(&self) -> RpcResult<Info> {
debug!("Getting node info via RPC");
let node_info = self.state.node.lock().await.info();
Ok(Info {
node_subnet: node_info.node_subnet.to_string(),
node_pubkey: node_info.node_pubkey,
})
}
async fn get_pubkey_from_ip(&self, ip_str: String) -> RpcResult<PublicKey> {
debug!(ip = %ip_str, "Getting public key from IP via RPC");
let ip = std::net::IpAddr::from_str(&ip_str)
.map_err(|_| ErrorObject::from(ErrorCode::from(-32007)))?;
let pubkey = self.state.node.lock().await.get_pubkey_from_ip(ip);
match pubkey {
Some(pk) => Ok(pk),
None => Err(ErrorObject::from(ErrorCode::from(-32008))),
}
}
async fn get_peers(&self) -> RpcResult<Vec<PeerStats>> {
debug!("Fetching peer stats via RPC");
let peers = self.state.node.lock().await.peer_info();
Ok(peers)
}
async fn add_peer(&self, endpoint_str: String) -> RpcResult<bool> {
debug!(
peer.endpoint = endpoint_str,
"Attempting to add peer to the system via RPC"
);
let endpoint = Endpoint::from_str(&endpoint_str)
.map_err(|_| ErrorObject::from(ErrorCode::from(-32009)))?;
match self.state.node.lock().await.add_peer(endpoint) {
Ok(()) => Ok(true),
Err(PeerExists) => Err(ErrorObject::from(ErrorCode::from(-32010))),
}
}
async fn delete_peer(&self, endpoint_str: String) -> RpcResult<bool> {
debug!(
peer.endpoint = endpoint_str,
"Attempting to remove peer from the system via RPC"
);
let endpoint = Endpoint::from_str(&endpoint_str)
.map_err(|_| ErrorObject::from(ErrorCode::from(-32012)))?;
match self.state.node.lock().await.remove_peer(endpoint) {
Ok(()) => Ok(true),
Err(PeerNotFound) => Err(ErrorObject::from(ErrorCode::from(-32011))),
}
}
async fn get_selected_routes(&self) -> RpcResult<Vec<Route>> {
debug!("Loading selected routes via RPC");
let routes = self
.state
.node
.lock()
.await
.selected_routes()
.into_iter()
.map(|sr| Route {
subnet: sr.source().subnet().to_string(),
next_hop: sr.neighbour().connection_identifier().clone(),
metric: if sr.metric().is_infinite() {
Metric::Infinite
} else {
Metric::Value(sr.metric().into())
},
seqno: sr.seqno().into(),
})
.collect();
Ok(routes)
}
async fn get_fallback_routes(&self) -> RpcResult<Vec<Route>> {
debug!("Loading fallback routes via RPC");
let routes = self
.state
.node
.lock()
.await
.fallback_routes()
.into_iter()
.map(|sr| Route {
subnet: sr.source().subnet().to_string(),
next_hop: sr.neighbour().connection_identifier().clone(),
metric: if sr.metric().is_infinite() {
Metric::Infinite
} else {
Metric::Value(sr.metric().into())
},
seqno: sr.seqno().into(),
})
.collect();
Ok(routes)
}
async fn get_queried_subnets(&self) -> RpcResult<Vec<QueriedSubnet>> {
debug!("Loading queried subnets via RPC");
let queries = self
.state
.node
.lock()
.await
.queried_subnets()
.into_iter()
.map(|qs| QueriedSubnet {
subnet: qs.subnet().to_string(),
expiration: qs
.query_expires()
.duration_since(tokio::time::Instant::now())
.as_secs()
.to_string(),
})
.collect();
Ok(queries)
}
async fn get_no_route_entries(&self) -> RpcResult<Vec<NoRouteSubnet>> {
debug!("Loading no route entries via RPC");
let entries = self
.state
.node
.lock()
.await
.no_route_entries()
.into_iter()
.map(|nrs| NoRouteSubnet {
subnet: nrs.subnet().to_string(),
expiration: nrs
.entry_expires()
.duration_since(tokio::time::Instant::now())
.as_secs()
.to_string(),
})
.collect();
Ok(entries)
}
async fn discover(&self) -> RpcResult<serde_json::Value> {
let spec = serde_json::from_str::<serde_json::Value>(OPENRPC_SPEC)
.expect("Failed to parse OpenRPC spec");
Ok(spec)
}
}
// Implement the message API trait only when the message feature is enabled
#[cfg(feature = "message")]
#[async_trait::async_trait]
impl<M> MyceliumMessageApiServer for RPCApi<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
async fn pop_message(
&self,
peek: Option<bool>,
timeout: Option<u64>,
topic: Option<String>,
) -> RpcResult<crate::message::MessageReceiveInfo> {
debug!(
"Attempt to get message via RPC, peek {}, timeout {} seconds",
peek.unwrap_or(false),
timeout.unwrap_or(0)
);
let topic_bytes = if let Some(topic_str) = topic {
Some(
base64::engine::general_purpose::STANDARD
.decode(topic_str.as_bytes())
.map_err(|_| ErrorObject::from(ErrorCode::from(-32013)))?,
)
} else {
None
};
// A timeout of 0 seconds essentially means get a message if there is one, and return
// immediately if there isn't.
let result = tokio::time::timeout(
Duration::from_secs(timeout.unwrap_or(0)),
self.state
.node
.lock()
.await
.get_message(!peek.unwrap_or(false), topic_bytes),
)
.await;
match result {
Ok(m) => Ok(crate::message::MessageReceiveInfo {
id: m.id,
src_ip: m.src_ip,
src_pk: m.src_pk,
dst_ip: m.dst_ip,
dst_pk: m.dst_pk,
topic: if m.topic.is_empty() {
None
} else {
Some(m.topic)
},
payload: m.data,
}),
_ => Err(ErrorObject::from(ErrorCode::from(-32014))),
}
}
async fn push_message(
&self,
message: crate::message::MessageSendInfo,
reply_timeout: Option<u64>,
) -> RpcResult<crate::message::PushMessageResponse> {
let dst = match message.dst {
crate::message::MessageDestination::Ip(ip) => ip,
crate::message::MessageDestination::Pk(pk) => pk.address().into(),
};
debug!(
message.dst=%dst,
message.len=message.payload.len(),
"Pushing new message via RPC",
);
// Default message try duration
const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5);
let result = self.state.node.lock().await.push_message(
dst,
message.payload,
message.topic,
DEFAULT_MESSAGE_TRY_DURATION,
reply_timeout.is_some(),
);
let (id, sub) = match result {
Ok((id, sub)) => (id, sub),
Err(_) => {
return Err(ErrorObject::from(ErrorCode::from(-32015)));
}
};
if reply_timeout.is_none() {
// If we don't wait for the reply just return here.
return Ok(crate::message::PushMessageResponse::Id(
crate::message::MessageIdReply { id },
));
}
let mut sub = sub.unwrap();
// Wait for reply with timeout
tokio::select! {
sub_res = sub.changed() => {
match sub_res {
Ok(_) => {
if let Some(m) = sub.borrow().deref() {
Ok(crate::message::PushMessageResponse::Reply(crate::message::MessageReceiveInfo {
id: m.id,
src_ip: m.src_ip,
src_pk: m.src_pk,
dst_ip: m.dst_ip,
dst_pk: m.dst_pk,
topic: if m.topic.is_empty() { None } else { Some(m.topic.clone()) },
payload: m.data.clone(),
}))
} else {
// This happens if a none value is send, which should not happen.
Err(ErrorObject::from(ErrorCode::from(-32016)))
}
}
Err(_) => {
// This happens if the sender drops, which should not happen.
Err(ErrorObject::from(ErrorCode::from(-32017)))
}
}
},
_ = tokio::time::sleep(Duration::from_secs(reply_timeout.unwrap_or(0))) => {
// Timeout expired while waiting for reply
Ok(crate::message::PushMessageResponse::Id(crate::message::MessageIdReply { id }))
}
}
}
async fn push_message_reply(
&self,
id: String,
message: crate::message::MessageSendInfo,
) -> RpcResult<bool> {
let message_id = match mycelium::message::MessageId::from_hex(id.as_bytes()) {
Ok(id) => id,
Err(_) => {
return Err(ErrorObject::from(ErrorCode::from(-32018)));
}
};
let dst = match message.dst {
crate::message::MessageDestination::Ip(ip) => ip,
crate::message::MessageDestination::Pk(pk) => pk.address().into(),
};
debug!(
message.id=id,
message.dst=%dst,
message.len=message.payload.len(),
"Pushing new reply to message via RPC",
);
// Default message try duration
const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5);
self.state.node.lock().await.reply_message(
message_id,
dst,
message.payload,
DEFAULT_MESSAGE_TRY_DURATION,
);
Ok(true)
}
async fn get_message_info(&self, id: String) -> RpcResult<mycelium::message::MessageInfo> {
let message_id = match mycelium::message::MessageId::from_hex(id.as_bytes()) {
Ok(id) => id,
Err(_) => {
return Err(ErrorObject::from(ErrorCode::from(-32020)));
}
};
debug!(message.id=%id, "Fetching message status via RPC");
let result = self.state.node.lock().await.message_status(message_id);
match result {
Some(info) => Ok(info),
None => Err(ErrorObject::from(ErrorCode::from(-32019))),
}
}
// Topic configuration methods implementation
async fn get_default_topic_action(&self) -> RpcResult<bool> {
debug!("Getting default topic action via RPC");
let accept = self.state.node.lock().await.unconfigure_topic_action();
Ok(accept)
}
async fn set_default_topic_action(&self, accept: bool) -> RpcResult<bool> {
debug!(accept=%accept, "Setting default topic action via RPC");
self.state
.node
.lock()
.await
.accept_unconfigured_topic(accept);
Ok(true)
}
async fn get_topics(&self) -> RpcResult<Vec<String>> {
debug!("Getting all whitelisted topics via RPC");
let topics = self
.state
.node
.lock()
.await
.topics()
.into_iter()
.map(|topic| base64::engine::general_purpose::STANDARD.encode(&topic))
.collect();
// For now, we'll return an empty list
Ok(topics)
}
async fn add_topic(&self, topic: String) -> RpcResult<bool> {
debug!("Adding topic to whitelist via RPC");
// Decode the base64 topic
let topic_bytes = base64::engine::general_purpose::STANDARD
.decode(topic.as_bytes())
.map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?;
self.state
.node
.lock()
.await
.add_topic_whitelist(topic_bytes);
Ok(true)
}
async fn remove_topic(&self, topic: String) -> RpcResult<bool> {
debug!("Removing topic from whitelist via RPC");
// Decode the base64 topic
let topic_bytes = base64::engine::general_purpose::STANDARD
.decode(topic.as_bytes())
.map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?;
self.state
.node
.lock()
.await
.remove_topic_whitelist(topic_bytes);
Ok(true)
}
async fn get_topic_sources(&self, topic: String) -> RpcResult<Vec<String>> {
debug!("Getting sources for topic via RPC");
// Decode the base64 topic
let topic_bytes = base64::engine::general_purpose::STANDARD
.decode(topic.as_bytes())
.map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?;
let subnets = self
.state
.node
.lock()
.await
.topic_allowed_sources(&topic_bytes)
.ok_or(ErrorObject::from(ErrorCode::from(-32030)))?
.into_iter()
.map(|subnet| subnet.to_string())
.collect();
Ok(subnets)
}
async fn add_topic_source(&self, topic: String, subnet: String) -> RpcResult<bool> {
debug!("Adding source to topic whitelist via RPC");
// Decode the base64 topic
let topic_bytes = base64::engine::general_purpose::STANDARD
.decode(topic.as_bytes())
.map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?;
// Parse the subnet
let subnet_obj = subnet
.parse::<Subnet>()
.map_err(|_| ErrorObject::from(ErrorCode::from(-32023)))?;
self.state
.node
.lock()
.await
.add_topic_whitelist_src(topic_bytes, subnet_obj);
Ok(true)
}
async fn remove_topic_source(&self, topic: String, subnet: String) -> RpcResult<bool> {
debug!("Removing source from topic whitelist via RPC");
// Decode the base64 topic
let topic_bytes = base64::engine::general_purpose::STANDARD
.decode(topic.as_bytes())
.map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?;
// Parse the subnet
let subnet_obj = subnet
.parse::<Subnet>()
.map_err(|_| ErrorObject::from(ErrorCode::from(-32023)))?;
self.state
.node
.lock()
.await
.remove_topic_whitelist_src(topic_bytes, subnet_obj);
Ok(true)
}
async fn get_topic_forward_socket(&self, topic: String) -> RpcResult<Option<String>> {
debug!("Getting forward socket for topic via RPC");
// Decode the base64 topic
let topic_bytes = base64::engine::general_purpose::STANDARD
.decode(topic.as_bytes())
.map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?;
let node = self.state.node.lock().await;
let socket_path = node
.get_topic_forward_socket(&topic_bytes)
.map(|p| p.to_string_lossy().to_string());
Ok(socket_path)
}
async fn set_topic_forward_socket(
&self,
topic: String,
socket_path: String,
) -> RpcResult<bool> {
debug!("Setting forward socket for topic via RPC");
// Decode the base64 topic
let topic_bytes = base64::engine::general_purpose::STANDARD
.decode(topic.as_bytes())
.map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?;
let path = PathBuf::from(socket_path);
self.state
.node
.lock()
.await
.set_topic_forward_socket(topic_bytes, path);
Ok(true)
}
async fn remove_topic_forward_socket(&self, topic: String) -> RpcResult<bool> {
debug!("Removing forward socket for topic via RPC");
// Decode the base64 topic
let topic_bytes = base64::engine::general_purpose::STANDARD
.decode(topic.as_bytes())
.map_err(|_| ErrorObject::from(ErrorCode::from(-32021)))?;
self.state
.node
.lock()
.await
.delete_topic_forward_socket(topic_bytes);
Ok(true)
}
}
/// JSON-RPC API server handle. The server is spawned in a background task. If this handle is dropped,
/// the server is terminated.
pub struct JsonRpc {
/// JSON-RPC server handle
_server: ServerHandle,
}
impl JsonRpc {
/// Spawns a new JSON-RPC API server on the provided listening address.
///
/// # Arguments
///
/// * `node` - The Mycelium node to use for the JSON-RPC API
/// * `listen_addr` - The address to listen on for JSON-RPC requests
///
/// # Returns
///
/// A `JsonRpc` instance that will be dropped when the server is terminated
pub async fn spawn<M>(node: Arc<Mutex<mycelium::Node<M>>>, listen_addr: SocketAddr) -> Self
where
M: Metrics + Clone + Send + Sync + 'static,
{
debug!(%listen_addr, "Starting JSON-RPC server");
let server_state = Arc::new(ServerState { node });
// Create the server builder
let server = ServerBuilder::default()
.build(listen_addr)
.await
.expect("Failed to build JSON-RPC server");
// Create the API implementation
let api = RPCApi {
state: server_state,
};
// Register the API implementation
// Create the RPC module
#[allow(unused_mut)]
let mut methods = MyceliumApiServer::into_rpc(api.clone());
// When the message feature is enabled, merge the message RPC module
#[cfg(feature = "message")]
{
let message_methods = MyceliumMessageApiServer::into_rpc(api);
methods
.merge(message_methods)
.expect("Can merge message API into base API");
}
// Start the server with the appropriate module(s)
let handle = server.start(methods);
debug!(%listen_addr, "JSON-RPC server started successfully");
JsonRpc { _server: handle }
}
}

View File

@@ -0,0 +1,64 @@
//! Admin-related JSON-RPC methods for the Mycelium API
use jsonrpc_core::{Error, ErrorCode, Result as RpcResult};
use std::net::IpAddr;
use std::str::FromStr;
use tracing::debug;
use mycelium::crypto::PublicKey;
use mycelium::metrics::Metrics;
use crate::HttpServerState;
use crate::Info;
use crate::rpc::models::error_codes;
use crate::rpc::traits::AdminApi;
/// Implementation of Admin-related JSON-RPC methods
pub struct AdminRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
state: HttpServerState<M>,
}
impl<M> AdminRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
/// Create a new AdminRpc instance
pub fn new(state: HttpServerState<M>) -> Self {
Self { state }
}
}
impl<M> AdminApi for AdminRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
fn get_info(&self) -> RpcResult<Info> {
debug!("Getting node info via RPC");
let info = self.state.node.blocking_lock().info();
Ok(Info {
node_subnet: info.node_subnet.to_string(),
node_pubkey: info.node_pubkey,
})
}
fn get_pubkey_from_ip(&self, mycelium_ip: String) -> RpcResult<PublicKey> {
debug!(ip = %mycelium_ip, "Getting public key from IP via RPC");
let ip = IpAddr::from_str(&mycelium_ip).map_err(|e| Error {
code: ErrorCode::InvalidParams,
message: format!("Invalid IP address: {}", e),
data: None,
})?;
match self.state.node.blocking_lock().get_pubkey_from_ip(ip) {
Some(pubkey) => Ok(pubkey),
None => Err(Error {
code: ErrorCode::ServerError(error_codes::PUBKEY_NOT_FOUND),
message: "Public key not found".to_string(),
data: None,
}),
}
}
}

View File

@@ -0,0 +1,263 @@
//! Message-related JSON-RPC methods for the Mycelium API
use jsonrpc_core::{Error, ErrorCode, Result as RpcResult};
use std::time::Duration;
use tracing::debug;
use mycelium::metrics::Metrics;
use mycelium::message::{MessageId, MessageInfo};
use crate::HttpServerState;
use crate::message::{MessageReceiveInfo, MessageSendInfo, PushMessageResponse};
use crate::rpc::models::error_codes;
use crate::rpc::traits::MessageApi;
/// Implementation of Message-related JSON-RPC methods
pub struct MessageRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
state: HttpServerState<M>,
}
impl<M> MessageRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
/// Create a new MessageRpc instance
pub fn new(state: HttpServerState<M>) -> Self {
Self { state }
}
/// Convert a base64 string to bytes
fn decode_base64(&self, s: &str) -> Result<Vec<u8>, Error> {
base64::engine::general_purpose::STANDARD.decode(s.as_bytes())
.map_err(|e| Error {
code: ErrorCode::InvalidParams,
message: format!("Invalid base64 encoding: {}", e),
data: None,
})
}
}
impl<M> MessageApi for MessageRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
fn pop_message(&self, peek: Option<bool>, timeout: Option<u64>, topic: Option<String>) -> RpcResult<MessageReceiveInfo> {
debug!(
"Attempt to get message via RPC, peek {}, timeout {} seconds",
peek.unwrap_or(false),
timeout.unwrap_or(0)
);
let topic_bytes = if let Some(topic_str) = topic {
Some(self.decode_base64(&topic_str)?)
} else {
None
};
// A timeout of 0 seconds essentially means get a message if there is one, and return
// immediately if there isn't.
let result = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
tokio::time::timeout(
Duration::from_secs(timeout.unwrap_or(0)),
self.state
.node
.lock()
.await
.get_message(!peek.unwrap_or(false), topic_bytes),
)
.await
})
});
match result {
Ok(Ok(m)) => Ok(MessageReceiveInfo {
id: m.id,
src_ip: m.src_ip,
src_pk: m.src_pk,
dst_ip: m.dst_ip,
dst_pk: m.dst_pk,
topic: if m.topic.is_empty() {
None
} else {
Some(m.topic)
},
payload: m.data,
}),
_ => Err(Error {
code: ErrorCode::ServerError(error_codes::NO_MESSAGE_READY),
message: "No message ready".to_string(),
data: None,
}),
}
}
fn push_message(&self, message: MessageSendInfo, reply_timeout: Option<u64>) -> RpcResult<PushMessageResponse> {
let dst = match message.dst {
crate::message::MessageDestination::Ip(ip) => ip,
crate::message::MessageDestination::Pk(pk) => pk.address().into(),
};
debug!(
message.dst=%dst,
message.len=message.payload.len(),
"Pushing new message via RPC",
);
// Default message try duration
const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5);
let result = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
self.state.node.lock().await.push_message(
dst,
message.payload,
message.topic,
DEFAULT_MESSAGE_TRY_DURATION,
reply_timeout.is_some(),
)
})
});
let (id, sub) = match result {
Ok((id, sub)) => (id, sub),
Err(_) => {
return Err(Error {
code: ErrorCode::InvalidParams,
message: "Failed to push message".to_string(),
data: None,
});
}
};
if reply_timeout.is_none() {
// If we don't wait for the reply just return here.
return Ok(PushMessageResponse::Id(crate::message::MessageIdReply { id }));
}
let mut sub = sub.unwrap();
// Wait for reply with timeout
let reply_result = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
tokio::select! {
sub_res = sub.changed() => {
match sub_res {
Ok(_) => {
if let Some(m) = sub.borrow().deref() {
Ok(PushMessageResponse::Reply(MessageReceiveInfo {
id: m.id,
src_ip: m.src_ip,
src_pk: m.src_pk,
dst_ip: m.dst_ip,
dst_pk: m.dst_pk,
topic: if m.topic.is_empty() { None } else { Some(m.topic.clone()) },
payload: m.data.clone(),
}))
} else {
// This happens if a none value is send, which should not happen.
Err(Error {
code: ErrorCode::InternalError,
message: "Internal error while waiting for reply".to_string(),
data: None,
})
}
}
Err(_) => {
// This happens if the sender drops, which should not happen.
Err(Error {
code: ErrorCode::InternalError,
message: "Internal error while waiting for reply".to_string(),
data: None,
})
}
}
},
_ = tokio::time::sleep(Duration::from_secs(reply_timeout.unwrap_or(0))) => {
// Timeout expired while waiting for reply
Ok(PushMessageResponse::Id(crate::message::MessageIdReply { id }))
}
}
})
});
match reply_result {
Ok(response) => Ok(response),
Err(e) => Err(e),
}
}
fn push_message_reply(&self, id: String, message: MessageSendInfo) -> RpcResult<bool> {
let message_id = match MessageId::from_hex(&id) {
Ok(id) => id,
Err(_) => {
return Err(Error {
code: ErrorCode::InvalidParams,
message: "Invalid message ID".to_string(),
data: None,
});
}
};
let dst = match message.dst {
crate::message::MessageDestination::Ip(ip) => ip,
crate::message::MessageDestination::Pk(pk) => pk.address().into(),
};
debug!(
message.id=id,
message.dst=%dst,
message.len=message.payload.len(),
"Pushing new reply to message via RPC",
);
// Default message try duration
const DEFAULT_MESSAGE_TRY_DURATION: Duration = Duration::from_secs(60 * 5);
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
self.state.node.lock().await.reply_message(
message_id,
dst,
message.payload,
DEFAULT_MESSAGE_TRY_DURATION,
);
})
});
Ok(true)
}
fn get_message_info(&self, id: String) -> RpcResult<MessageInfo> {
let message_id = match MessageId::from_hex(&id) {
Ok(id) => id,
Err(_) => {
return Err(Error {
code: ErrorCode::InvalidParams,
message: "Invalid message ID".to_string(),
data: None,
});
}
};
debug!(message.id=%id, "Fetching message status via RPC");
let result = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
self.state.node.lock().await.message_status(message_id)
})
});
match result {
Some(info) => Ok(info),
None => Err(Error {
code: ErrorCode::ServerError(error_codes::MESSAGE_NOT_FOUND),
message: "Message not found".to_string(),
data: None,
}),
}
}
}

View File

@@ -0,0 +1,30 @@
//! Models for the Mycelium JSON-RPC API
use serde::{Deserialize, Serialize};
// Define any additional models needed for the JSON-RPC API
// Most models can be reused from the existing REST API
/// Error codes for the JSON-RPC API
pub mod error_codes {
/// Invalid parameters error code
pub const INVALID_PARAMS: i64 = -32602;
/// Peer already exists error code
pub const PEER_EXISTS: i64 = 409;
/// Peer not found error code
pub const PEER_NOT_FOUND: i64 = 404;
/// Message not found error code
pub const MESSAGE_NOT_FOUND: i64 = 404;
/// Public key not found error code
pub const PUBKEY_NOT_FOUND: i64 = 404;
/// No message ready error code
pub const NO_MESSAGE_READY: i64 = 204;
/// Timeout waiting for reply error code
pub const TIMEOUT_WAITING_FOR_REPLY: i64 = 408;
}

View File

@@ -0,0 +1,86 @@
//! Peer-related JSON-RPC methods for the Mycelium API
use jsonrpc_core::{Error, ErrorCode, Result as RpcResult};
use std::str::FromStr;
use tracing::debug;
use mycelium::endpoint::Endpoint;
use mycelium::metrics::Metrics;
use mycelium::peer_manager::{PeerExists, PeerNotFound, PeerStats};
use crate::rpc::models::error_codes;
use crate::rpc::traits::PeerApi;
use crate::HttpServerState;
/// Implementation of Peer-related JSON-RPC methods
pub struct PeerRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
state: HttpServerState<M>,
}
impl<M> PeerRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
/// Create a new PeerRpc instance
pub fn new(state: HttpServerState<M>) -> Self {
Self { state }
}
}
impl<M> PeerApi for PeerRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
fn get_peers(&self) -> RpcResult<Vec<PeerStats>> {
debug!("Fetching peer stats via RPC");
Ok(self.state.node.blocking_lock().peer_info())
}
fn add_peer(&self, endpoint: String) -> RpcResult<bool> {
debug!(
peer.endpoint = endpoint,
"Attempting to add peer to the system via RPC"
);
let endpoint = Endpoint::from_str(&endpoint).map_err(|e| Error {
code: ErrorCode::InvalidParams,
message: e.to_string(),
data: None,
})?;
match self.state.node.blocking_lock().add_peer(endpoint) {
Ok(()) => Ok(true),
Err(PeerExists) => Err(Error {
code: ErrorCode::ServerError(error_codes::PEER_EXISTS),
message: "A peer identified by that endpoint already exists".to_string(),
data: None,
}),
}
}
fn delete_peer(&self, endpoint: String) -> RpcResult<bool> {
debug!(
peer.endpoint = endpoint,
"Attempting to remove peer from the system via RPC"
);
let endpoint = Endpoint::from_str(&endpoint).map_err(|e| Error {
code: ErrorCode::InvalidParams,
message: e.to_string(),
data: None,
})?;
match self.state.node.blocking_lock().remove_peer(endpoint) {
Ok(()) => Ok(true),
Err(PeerNotFound) => Err(Error {
code: ErrorCode::ServerError(error_codes::PEER_NOT_FOUND),
message: "A peer identified by that endpoint does not exist".to_string(),
data: None,
}),
}
}
}

View File

@@ -0,0 +1,120 @@
//! Route-related JSON-RPC methods for the Mycelium API
use jsonrpc_core::Result as RpcResult;
use tracing::debug;
use mycelium::metrics::Metrics;
use crate::HttpServerState;
use crate::Route;
use crate::QueriedSubnet;
use crate::NoRouteSubnet;
use crate::Metric;
use crate::rpc::traits::RouteApi;
/// Implementation of Route-related JSON-RPC methods
pub struct RouteRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
state: HttpServerState<M>,
}
impl<M> RouteRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
/// Create a new RouteRpc instance
pub fn new(state: HttpServerState<M>) -> Self {
Self { state }
}
}
impl<M> RouteApi for RouteRpc<M>
where
M: Metrics + Clone + Send + Sync + 'static,
{
fn get_selected_routes(&self) -> RpcResult<Vec<Route>> {
debug!("Loading selected routes via RPC");
let routes = self.state
.node
.blocking_lock()
.selected_routes()
.into_iter()
.map(|sr| Route {
subnet: sr.source().subnet().to_string(),
next_hop: sr.neighbour().connection_identifier().clone(),
metric: if sr.metric().is_infinite() {
Metric::Infinite
} else {
Metric::Value(sr.metric().into())
},
seqno: sr.seqno().into(),
})
.collect();
Ok(routes)
}
fn get_fallback_routes(&self) -> RpcResult<Vec<Route>> {
debug!("Loading fallback routes via RPC");
let routes = self.state
.node
.blocking_lock()
.fallback_routes()
.into_iter()
.map(|sr| Route {
subnet: sr.source().subnet().to_string(),
next_hop: sr.neighbour().connection_identifier().clone(),
metric: if sr.metric().is_infinite() {
Metric::Infinite
} else {
Metric::Value(sr.metric().into())
},
seqno: sr.seqno().into(),
})
.collect();
Ok(routes)
}
fn get_queried_subnets(&self) -> RpcResult<Vec<QueriedSubnet>> {
debug!("Loading queried subnets via RPC");
let queries = self.state
.node
.blocking_lock()
.queried_subnets()
.into_iter()
.map(|qs| QueriedSubnet {
subnet: qs.subnet().to_string(),
expiration: qs
.query_expires()
.duration_since(tokio::time::Instant::now())
.as_secs()
.to_string(),
})
.collect();
Ok(queries)
}
fn get_no_route_entries(&self) -> RpcResult<Vec<NoRouteSubnet>> {
debug!("Loading no route entries via RPC");
let entries = self.state
.node
.blocking_lock()
.no_route_entries()
.into_iter()
.map(|nrs| NoRouteSubnet {
subnet: nrs.subnet().to_string(),
expiration: nrs
.entry_expires()
.duration_since(tokio::time::Instant::now())
.as_secs()
.to_string(),
})
.collect();
Ok(entries)
}
}

View File

@@ -0,0 +1,4 @@
//! OpenRPC specification for the Mycelium JSON-RPC API
/// The OpenRPC specification for the Mycelium JSON-RPC API
pub const OPENRPC_SPEC: &str = include_str!("../../../docs/openrpc.json");

View File

@@ -0,0 +1,80 @@
//! RPC trait definitions for the Mycelium JSON-RPC API
use jsonrpc_core::Result as RpcResult;
use jsonrpc_derive::rpc;
use crate::Info;
use crate::Route;
use crate::QueriedSubnet;
use crate::NoRouteSubnet;
use mycelium::crypto::PublicKey;
use mycelium::peer_manager::PeerStats;
use mycelium::message::{MessageId, MessageInfo};
// Admin-related RPC methods
#[rpc]
pub trait AdminApi {
/// Get general info about the node
#[rpc(name = "getInfo")]
fn get_info(&self) -> RpcResult<Info>;
/// Get the pubkey from node ip
#[rpc(name = "getPublicKeyFromIp")]
fn get_pubkey_from_ip(&self, mycelium_ip: String) -> RpcResult<PublicKey>;
}
// Peer-related RPC methods
#[rpc]
pub trait PeerApi {
/// List known peers
#[rpc(name = "getPeers")]
fn get_peers(&self) -> RpcResult<Vec<PeerStats>>;
/// Add a new peer
#[rpc(name = "addPeer")]
fn add_peer(&self, endpoint: String) -> RpcResult<bool>;
/// Remove an existing peer
#[rpc(name = "deletePeer")]
fn delete_peer(&self, endpoint: String) -> RpcResult<bool>;
}
// Route-related RPC methods
#[rpc]
pub trait RouteApi {
/// List all selected routes
#[rpc(name = "getSelectedRoutes")]
fn get_selected_routes(&self) -> RpcResult<Vec<Route>>;
/// List all active fallback routes
#[rpc(name = "getFallbackRoutes")]
fn get_fallback_routes(&self) -> RpcResult<Vec<Route>>;
/// List all currently queried subnets
#[rpc(name = "getQueriedSubnets")]
fn get_queried_subnets(&self) -> RpcResult<Vec<QueriedSubnet>>;
/// List all subnets which are explicitly marked as no route
#[rpc(name = "getNoRouteEntries")]
fn get_no_route_entries(&self) -> RpcResult<Vec<NoRouteSubnet>>;
}
// Message-related RPC methods
#[rpc]
pub trait MessageApi {
/// Get a message from the inbound message queue
#[rpc(name = "popMessage")]
fn pop_message(&self, peek: Option<bool>, timeout: Option<u64>, topic: Option<String>) -> RpcResult<crate::message::MessageReceiveInfo>;
/// Submit a new message to the system
#[rpc(name = "pushMessage")]
fn push_message(&self, message: crate::message::MessageSendInfo, reply_timeout: Option<u64>) -> RpcResult<crate::message::PushMessageResponse>;
/// Reply to a message with the given ID
#[rpc(name = "pushMessageReply")]
fn push_message_reply(&self, id: String, message: crate::message::MessageSendInfo) -> RpcResult<bool>;
/// Get the status of an outbound message
#[rpc(name = "getMessageInfo")]
fn get_message_info(&self, id: String) -> RpcResult<MessageInfo>;
}