From 6f9cf510da6cc9f7a0b48e7314b997e1fa6ebd8f Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Mon, 14 Nov 2022 23:24:35 -0500 Subject: [PATCH] Support an authenticated Monero RPC Closes https://github.com/serai-dex/serai/issues/143. --- Cargo.lock | 27 ++++++++++++- coins/monero/Cargo.toml | 1 + coins/monero/src/rpc.rs | 73 +++++++++++++++++++++++++++++++----- coins/monero/tests/rpc.rs | 2 +- processor/src/coin/monero.rs | 2 +- 5 files changed, 93 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c0a1606..03d9873c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1594,6 +1594,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "digest_auth" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa30657988b2ced88f68fe490889e739bf98d342916c33ed3100af1d6f1cbc9c" +dependencies = [ + "digest 0.9.0", + "hex", + "md-5 0.9.1", + "rand 0.8.5", + "sha2 0.9.9", +] + [[package]] name = "directories" version = "4.0.1" @@ -2206,7 +2219,7 @@ dependencies = [ "glob", "hex", "home", - "md-5", + "md-5 0.10.5", "num_cpus", "once_cell", "path-slash", @@ -4423,6 +4436,17 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + [[package]] name = "md-5" version = "0.10.5" @@ -4638,6 +4662,7 @@ dependencies = [ "blake2", "curve25519-dalek 3.2.0", "dalek-ff-group", + "digest_auth", "dleq", "flexible-transcript", "group", diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index ca42c5f6..6525ab25 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -46,6 +46,7 @@ serde_json = "1.0" base58-monero = "1" monero-epee-bin-serde = "1.0" +digest_auth = "0.3" reqwest = { version = "0.11", features = ["json"] } [build-dependencies] diff --git a/coins/monero/src/rpc.rs b/coins/monero/src/rpc.rs index 73c0dbc9..a5201535 100644 --- a/coins/monero/src/rpc.rs +++ b/coins/monero/src/rpc.rs @@ -7,7 +7,8 @@ use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; use serde::{Serialize, Deserialize, de::DeserializeOwned}; use serde_json::{Value, json}; -use reqwest; +use digest_auth::AuthContext; +use reqwest::{Client, RequestBuilder}; use crate::{ Protocol, @@ -72,11 +73,47 @@ fn rpc_point(point: &str) -> Result { } #[derive(Clone, Debug)] -pub struct Rpc(String); +pub struct Rpc { + client: Client, + userpass: Option<(String, String)>, + url: String, +} impl Rpc { - pub fn new(daemon: String) -> Rpc { - Rpc(daemon) + /// Create a new RPC connection. + /// A daemon requiring authentication can be used via including the username and password in the + /// URL. + pub fn new(mut url: String) -> Result { + // Parse out the username and password + let userpass = if url.contains('@') { + let url_clone = url.clone(); + let split_url = url_clone.split('@').collect::>(); + if split_url.len() != 2 { + Err(RpcError::InvalidNode)?; + } + let mut userpass = split_url[0]; + url = split_url[1].to_string(); + + // If there was additionally a protocol string, restore that to the daemon URL + if userpass.contains("://") { + let split_userpass = userpass.split("://").collect::>(); + if split_userpass.len() != 2 { + Err(RpcError::InvalidNode)?; + } + url = split_userpass[0].to_string() + "://" + &url; + userpass = split_userpass[1]; + } + + let split_userpass = userpass.split(':').collect::>(); + if split_userpass.len() != 2 { + Err(RpcError::InvalidNode)?; + } + Some((split_userpass[0].to_string(), split_userpass[1].to_string())) + } else { + None + }; + + Ok(Rpc { client: Client::new(), userpass, url }) } /// Perform a RPC call to the specified method with the provided parameters. @@ -87,8 +124,7 @@ impl Rpc { method: &str, params: Option, ) -> Result { - let client = reqwest::Client::new(); - let mut builder = client.post(self.0.clone() + "/" + method); + let mut builder = self.client.post(self.url.clone() + "/" + method); if let Some(params) = params.as_ref() { builder = builder.json(params); } @@ -115,16 +151,35 @@ impl Rpc { method: &str, params: Vec, ) -> Result { - let client = reqwest::Client::new(); - let builder = client.post(self.0.clone() + "/" + method).body(params); + let builder = self.client.post(self.url.clone() + "/" + method).body(params.clone()); self.call_tail(method, builder.header("Content-Type", "application/octet-stream")).await } async fn call_tail( &self, method: &str, - builder: reqwest::RequestBuilder, + mut builder: RequestBuilder, ) -> Result { + if let Some((user, pass)) = &self.userpass { + let req = self.client.post(&self.url).send().await.map_err(|_| RpcError::InvalidNode)?; + // Only provide authentication if this daemon actually expects it + if let Some(header) = req.headers().get("www-authenticate") { + builder = builder.header( + "Authorization", + digest_auth::parse(header.to_str().map_err(|_| RpcError::InvalidNode)?) + .map_err(|_| RpcError::InvalidNode)? + .respond(&AuthContext::new_post::<_, _, _, &[u8]>( + user, + pass, + "/".to_string() + method, + None, + )) + .map_err(|_| RpcError::InvalidNode)? + .to_header_string(), + ); + } + } + let res = builder.send().await.map_err(|_| RpcError::ConnectionError)?; Ok(if !method.ends_with(".bin") { diff --git a/coins/monero/tests/rpc.rs b/coins/monero/tests/rpc.rs index 6443252b..819de585 100644 --- a/coins/monero/tests/rpc.rs +++ b/coins/monero/tests/rpc.rs @@ -11,7 +11,7 @@ use monero_serai::{ }; pub async fn rpc() -> Rpc { - let rpc = Rpc::new("http://127.0.0.1:18081".to_string()); + let rpc = Rpc::new("http://127.0.0.1:18081".to_string()).unwrap(); // Only run once if rpc.get_height().await.unwrap() != 1 { diff --git a/processor/src/coin/monero.rs b/processor/src/coin/monero.rs index 322fa3c6..fbca66ae 100644 --- a/processor/src/coin/monero.rs +++ b/processor/src/coin/monero.rs @@ -70,7 +70,7 @@ pub struct Monero { impl Monero { pub async fn new(url: String) -> Monero { - Monero { rpc: Rpc::new(url), view: additional_key::(0).0 } + Monero { rpc: Rpc::new(url).unwrap(), view: additional_key::(0).0 } } fn scanner(&self, spend: dfg::EdwardsPoint) -> Scanner {