Squashed 'components/mycelium/' content from commit afb32e0
git-subtree-dir: components/mycelium git-subtree-split: afb32e0cdb2d4cdd17f22a5693278068d061f08c
This commit is contained in:
37
mycelium-api/Cargo.toml
Normal file
37
mycelium-api/Cargo.toml
Normal 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
492
mycelium-api/src/lib.rs
Normal 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
652
mycelium-api/src/message.rs
Normal 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
752
mycelium-api/src/rpc.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
64
mycelium-api/src/rpc/admin.rs
Normal file
64
mycelium-api/src/rpc/admin.rs
Normal 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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
263
mycelium-api/src/rpc/message.rs
Normal file
263
mycelium-api/src/rpc/message.rs
Normal 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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
30
mycelium-api/src/rpc/models.rs
Normal file
30
mycelium-api/src/rpc/models.rs
Normal 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;
|
||||
}
|
||||
86
mycelium-api/src/rpc/peer.rs
Normal file
86
mycelium-api/src/rpc/peer.rs
Normal 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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
120
mycelium-api/src/rpc/route.rs
Normal file
120
mycelium-api/src/rpc/route.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
4
mycelium-api/src/rpc/spec.rs
Normal file
4
mycelium-api/src/rpc/spec.rs
Normal 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");
|
||||
80
mycelium-api/src/rpc/traits.rs
Normal file
80
mycelium-api/src/rpc/traits.rs
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user