| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- /* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2025 Dyne.org foundation
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
- use std::sync::Arc;
- use log::{debug, error};
- use smol::{channel, io::BufReader, Executor};
- use tinyjson::JsonValue;
- use url::Url;
- use super::{
- common::{
- http_read_from_stream_response, http_write_to_stream, read_from_stream, write_to_stream,
- INIT_BUF_SIZE, READ_TIMEOUT,
- },
- jsonrpc::*,
- };
- use crate::{
- net::transport::{Dialer, PtStream},
- system::{io_timeout, PublisherPtr, StoppableTask, StoppableTaskPtr},
- Error, Result,
- };
- /// JSON-RPC client implementation using asynchronous channels.
- pub struct RpcClient {
- /// The channel used to send JSON-RPC request objects.
- /// The `bool` marks if we should have a reply read timeout.
- req_send: channel::Sender<(JsonRequest, bool)>,
- /// The channel used to read the JSON-RPC response object.
- rep_recv: channel::Receiver<JsonResult>,
- /// The channel used to skip waiting for a JSON-RPC client request
- req_skip_send: channel::Sender<()>,
- /// The stoppable task pointer, used on [`RpcClient::stop()`]
- task: StoppableTaskPtr,
- }
- impl RpcClient {
- /// Instantiate a new JSON-RPC client that connects to the given endpoint.
- /// The function takes an `Executor` object, which is needed to start the
- /// `StoppableTask` which represents the client-server connection.
- pub async fn new(endpoint: Url, ex: Arc<Executor<'_>>) -> Result<Self> {
- // Instantiate communication channels
- let (req_send, req_recv) = channel::unbounded();
- let (rep_send, rep_recv) = channel::unbounded();
- let (req_skip_send, req_skip_recv) = channel::unbounded();
- // Figure out if we're using HTTP and rewrite the URL accordingly.
- let mut dialer_url = endpoint.clone();
- if endpoint.scheme().starts_with("http+") {
- let scheme = endpoint.scheme().strip_prefix("http+").unwrap();
- let url_str = endpoint.as_str().replace(endpoint.scheme(), scheme);
- dialer_url = url_str.parse()?;
- }
- let use_http = endpoint.scheme().starts_with("http+");
- // Instantiate Dialer and dial the server
- // TODO: Could add a timeout here
- let dialer = Dialer::new(dialer_url, None).await?;
- let stream = dialer.dial(None).await?;
- // Create the StoppableTask running the request-reply loop.
- // This represents the actual connection, which can be stopped
- // using `RpcClient::stop()`.
- let task = StoppableTask::new();
- task.clone().start(
- Self::reqrep_loop(use_http, stream, rep_send, req_recv, req_skip_recv),
- |res| async move {
- match res {
- Ok(()) | Err(Error::RpcClientStopped) => {}
- Err(e) => error!(target: "rpc::client", "[RPC] Client error: {}", e),
- }
- },
- Error::RpcClientStopped,
- ex.clone(),
- );
- Ok(Self { req_send, rep_recv, task, req_skip_send })
- }
- /// Stop the JSON-RPC client. This will trigger `stop()` on the inner
- /// `StoppableTaskPtr` resulting in stopping the internal reqrep loop
- /// and therefore closing the connection.
- pub async fn stop(&self) {
- self.task.stop().await;
- }
- /// Internal function that loops on a given stream and multiplexes the data
- async fn reqrep_loop(
- use_http: bool,
- stream: Box<dyn PtStream>,
- rep_send: channel::Sender<JsonResult>,
- req_recv: channel::Receiver<(JsonRequest, bool)>,
- req_skip_recv: channel::Receiver<()>,
- ) -> Result<()> {
- debug!(target: "rpc::client::reqrep_loop()", "Starting reqrep loop");
- let (reader, mut writer) = smol::io::split(stream);
- let mut reader = BufReader::new(reader);
- loop {
- let mut buf = Vec::with_capacity(INIT_BUF_SIZE);
- let mut with_timeout = false;
- // Read an incoming client request, or skip it if triggered from
- // a JSONRPC notification subscriber
- smol::future::or(
- async {
- let (request, timeout) = req_recv.recv().await?;
- with_timeout = timeout;
- let request = JsonResult::Request(request);
- if use_http {
- http_write_to_stream(&mut writer, &request).await?;
- } else {
- write_to_stream(&mut writer, &request).await?;
- }
- Ok::<(), crate::Error>(())
- },
- async {
- req_skip_recv.recv().await?;
- Ok::<(), crate::Error>(())
- },
- )
- .await?;
- if with_timeout {
- if use_http {
- let _ = io_timeout(
- READ_TIMEOUT,
- http_read_from_stream_response(&mut reader, &mut buf),
- )
- .await?;
- } else {
- let _ =
- io_timeout(READ_TIMEOUT, read_from_stream(&mut reader, &mut buf)).await?;
- }
- } else {
- #[allow(clippy::collapsible_else_if)]
- if use_http {
- let _ = http_read_from_stream_response(&mut reader, &mut buf).await?;
- } else {
- let _ = read_from_stream(&mut reader, &mut buf).await?;
- }
- }
- let val: JsonValue = String::from_utf8(buf)?.parse()?;
- let rep = JsonResult::try_from_value(&val)?;
- rep_send.send(rep).await?;
- }
- }
- /// Send a given JSON-RPC request over the instantiated client and
- /// return a possible result. If the response is an error, returns
- /// a `JsonRpcError`.
- pub async fn request(&self, req: JsonRequest) -> Result<JsonValue> {
- let req_id = req.id;
- debug!(target: "rpc::client", "--> {}", req.stringify()?);
- // If the connection is closed, the sender will get an error
- // for sending to a closed channel.
- self.req_send.send((req, true)).await?;
- // If the connection is closed, the receiver will get an error
- // for waiting on a closed channel.
- let reply = self.rep_recv.recv().await?;
- // Handle the response
- match reply {
- JsonResult::Response(rep) | JsonResult::SubscriberWithReply(_, rep) => {
- debug!(target: "rpc::client", "<-- {}", rep.stringify()?);
- // Check if the IDs match
- if req_id != rep.id {
- let e = JsonError::new(ErrorCode::IdMismatch, None, rep.id);
- return Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- Ok(rep.result)
- }
- JsonResult::Error(e) => {
- debug!(target: "rpc::client", "<-- {}", e.stringify()?);
- Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- JsonResult::Notification(n) => {
- debug!(target: "rpc::client", "<-- {}", n.stringify()?);
- let e = JsonError::new(ErrorCode::InvalidReply, None, req_id);
- Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- JsonResult::Request(r) => {
- debug!(target: "rpc::client", "<-- {}", r.stringify()?);
- let e = JsonError::new(ErrorCode::InvalidReply, None, req_id);
- Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- JsonResult::Subscriber(_) => {
- // When?
- let e = JsonError::new(ErrorCode::InvalidReply, None, req_id);
- Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- }
- }
- /// Oneshot send a given JSON-RPC request over the instantiated client
- /// and immediately close the channels upon receiving a reply.
- pub async fn oneshot_request(&self, req: JsonRequest) -> Result<JsonValue> {
- let rep = match self.request(req).await {
- Ok(v) => v,
- Err(e) => {
- self.stop().await;
- return Err(e)
- }
- };
- self.stop().await;
- Ok(rep)
- }
- /// Listen instantiated client for notifications.
- /// NOTE: Subscriber listeners must perform response handling.
- pub async fn subscribe(
- &self,
- req: JsonRequest,
- publisher: PublisherPtr<JsonResult>,
- ) -> Result<()> {
- // Perform initial request
- debug!(target: "rpc::client", "--> {}", req.stringify()?);
- let req_id = req.id;
- // If the connection is closed, the sender will get an error for
- // sending to a closed channel.
- self.req_send.send((req, false)).await?;
- // Now loop and listen to notifications
- loop {
- // If the connection is closed, the receiver will get an error
- // for waiting on a closed channel.
- let notification = self.rep_recv.recv().await?;
- // Handle the response
- match notification {
- JsonResult::Notification(ref n) => {
- debug!(target: "rpc::client", "<-- {}", n.stringify()?);
- self.req_skip_send.send(()).await?;
- publisher.notify(notification.clone()).await;
- continue
- }
- JsonResult::Error(e) => {
- debug!(target: "rpc::client", "<-- {}", e.stringify()?);
- return Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- JsonResult::Response(r) | JsonResult::SubscriberWithReply(_, r) => {
- debug!(target: "rpc::client", "<-- {}", r.stringify()?);
- let e = JsonError::new(ErrorCode::InvalidReply, None, req_id);
- return Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- JsonResult::Request(r) => {
- debug!(target: "rpc::client", "<-- {}", r.stringify()?);
- let e = JsonError::new(ErrorCode::InvalidReply, None, req_id);
- return Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- JsonResult::Subscriber(_) => {
- // When?
- let e = JsonError::new(ErrorCode::InvalidReply, None, req_id);
- return Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- }
- }
- }
- }
- /// Highly experimental JSON-RPC client implementation using asynchronous channels,
- /// with each new request canceling waiting for the previous one. All requests are
- /// executed without a timeout.
- pub struct RpcChadClient {
- /// The channel used to send JSON-RPC request objects
- req_send: channel::Sender<JsonRequest>,
- /// The channel used to read the JSON-RPC response object
- rep_recv: channel::Receiver<JsonResult>,
- /// The stoppable task pointer, used on [`RpcChadClient::stop()`]
- task: StoppableTaskPtr,
- }
- impl RpcChadClient {
- /// Instantiate a new JSON-RPC client that connects to the given endpoint.
- /// The function takes an `Executor` object, which is needed to start the
- /// `StoppableTask` which represents the client-server connection.
- pub async fn new(endpoint: Url, ex: Arc<Executor<'_>>) -> Result<Self> {
- // Instantiate communication channels
- let (req_send, req_recv) = channel::unbounded();
- let (rep_send, rep_recv) = channel::unbounded();
- // Figure out if we're using HTTP and rewrite the URL accordingly.
- let mut dialer_url = endpoint.clone();
- if endpoint.scheme().starts_with("http+") {
- let scheme = endpoint.scheme().strip_prefix("http+").unwrap();
- let url_str = endpoint.as_str().replace(endpoint.scheme(), scheme);
- dialer_url = url_str.parse()?;
- }
- let use_http = endpoint.scheme().starts_with("http+");
- // Instantiate Dialer and dial the server
- // TODO: Could add a timeout here
- let dialer = Dialer::new(dialer_url, None).await?;
- let stream = dialer.dial(None).await?;
- // Create the StoppableTask running the request-reply loop.
- // This represents the actual connection, which can be stopped
- // using `RpcChadClient::stop()`.
- let task = StoppableTask::new();
- task.clone().start(
- Self::reqrep_loop(use_http, stream, rep_send, req_recv),
- |res| async move {
- match res {
- Ok(()) | Err(Error::RpcClientStopped) => {}
- Err(e) => error!(target: "rpc::chad_client", "[RPC] Client error: {}", e),
- }
- },
- Error::RpcClientStopped,
- ex.clone(),
- );
- Ok(Self { req_send, rep_recv, task })
- }
- /// Stop the JSON-RPC client. This will trigger `stop()` on the inner
- /// `StoppableTaskPtr` resulting in stopping the internal reqrep loop
- /// and therefore closing the connection.
- pub async fn stop(&self) {
- self.task.stop().await;
- }
- /// Internal function that loops on a given stream and multiplexes the data
- async fn reqrep_loop(
- use_http: bool,
- stream: Box<dyn PtStream>,
- rep_send: channel::Sender<JsonResult>,
- req_recv: channel::Receiver<JsonRequest>,
- ) -> Result<()> {
- debug!(target: "rpc::chad_client::reqrep_loop()", "Starting reqrep loop");
- let (reader, mut writer) = smol::io::split(stream);
- let mut reader = BufReader::new(reader);
- loop {
- let mut buf = Vec::with_capacity(INIT_BUF_SIZE);
- // Read an incoming client request, or wait for a response
- smol::future::or(
- async {
- let request = req_recv.recv().await?;
- let request = JsonResult::Request(request);
- if use_http {
- http_write_to_stream(&mut writer, &request).await?;
- } else {
- write_to_stream(&mut writer, &request).await?;
- }
- Ok::<(), crate::Error>(())
- },
- async {
- if use_http {
- let _ = http_read_from_stream_response(&mut reader, &mut buf).await?;
- } else {
- let _ = read_from_stream(&mut reader, &mut buf).await?;
- }
- let val: JsonValue = String::from_utf8(buf)?.parse()?;
- let rep = JsonResult::try_from_value(&val)?;
- rep_send.send(rep).await?;
- Ok::<(), crate::Error>(())
- },
- )
- .await?;
- }
- }
- /// Send a given JSON-RPC request over the instantiated client and
- /// return a possible result. If the response is an error, returns
- /// a `JsonRpcError`.
- pub async fn request(&self, req: JsonRequest) -> Result<JsonValue> {
- // Perform request
- let req_id = req.id;
- debug!(target: "rpc::chad_client", "--> {}", req.stringify()?);
- // If the connection is closed, the sender will get an error
- // for sending to a closed channel.
- self.req_send.send(req).await?;
- // Now loop until we receive our response
- loop {
- // If the connection is closed, the receiver will get an error
- // for waiting on a closed channel.
- let reply = self.rep_recv.recv().await?;
- // Handle the response
- match reply {
- JsonResult::Response(rep) | JsonResult::SubscriberWithReply(_, rep) => {
- debug!(target: "rpc::chad_client", "<-- {}", rep.stringify()?);
- // Check if the IDs match
- if req_id != rep.id {
- debug!(target: "rpc::chad_client", "Skipping response for request {} as its not our latest({})", rep.id, req_id);
- continue
- }
- return Ok(rep.result)
- }
- JsonResult::Error(e) => {
- debug!(target: "rpc::chad_client", "<-- {}", e.stringify()?);
- // Check if the IDs match
- if req_id != e.id {
- debug!(target: "rpc::chad_client", "Skipping response for request {} as its not our latest({})", e.id, req_id);
- continue
- }
- return Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- JsonResult::Notification(n) => {
- debug!(target: "rpc::chad_client", "<-- {}", n.stringify()?);
- let e = JsonError::new(ErrorCode::InvalidReply, None, req_id);
- return Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- JsonResult::Request(r) => {
- debug!(target: "rpc::chad_client", "<-- {}", r.stringify()?);
- let e = JsonError::new(ErrorCode::InvalidReply, None, req_id);
- return Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- JsonResult::Subscriber(_) => {
- // When?
- let e = JsonError::new(ErrorCode::InvalidReply, None, req_id);
- return Err(Error::JsonRpcError((e.error.code, e.error.message)))
- }
- }
- }
- }
- }
|