Support an authenticated Monero RPC

Closes https://github.com/serai-dex/serai/issues/143.
This commit is contained in:
Luke Parker 2022-11-14 23:24:35 -05:00
parent b05a223b69
commit 6f9cf510da
No known key found for this signature in database
GPG key ID: F9F1386DB1E119B6
5 changed files with 93 additions and 12 deletions

27
Cargo.lock generated
View file

@ -1594,6 +1594,19 @@ dependencies = [
"subtle", "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]] [[package]]
name = "directories" name = "directories"
version = "4.0.1" version = "4.0.1"
@ -2206,7 +2219,7 @@ dependencies = [
"glob", "glob",
"hex", "hex",
"home", "home",
"md-5", "md-5 0.10.5",
"num_cpus", "num_cpus",
"once_cell", "once_cell",
"path-slash", "path-slash",
@ -4423,6 +4436,17 @@ dependencies = [
"rawpointer", "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]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.5" version = "0.10.5"
@ -4638,6 +4662,7 @@ dependencies = [
"blake2", "blake2",
"curve25519-dalek 3.2.0", "curve25519-dalek 3.2.0",
"dalek-ff-group", "dalek-ff-group",
"digest_auth",
"dleq", "dleq",
"flexible-transcript", "flexible-transcript",
"group", "group",

View file

@ -46,6 +46,7 @@ serde_json = "1.0"
base58-monero = "1" base58-monero = "1"
monero-epee-bin-serde = "1.0" monero-epee-bin-serde = "1.0"
digest_auth = "0.3"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json"] }
[build-dependencies] [build-dependencies]

View file

@ -7,7 +7,8 @@ use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
use serde::{Serialize, Deserialize, de::DeserializeOwned}; use serde::{Serialize, Deserialize, de::DeserializeOwned};
use serde_json::{Value, json}; use serde_json::{Value, json};
use reqwest; use digest_auth::AuthContext;
use reqwest::{Client, RequestBuilder};
use crate::{ use crate::{
Protocol, Protocol,
@ -72,11 +73,47 @@ fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Rpc(String); pub struct Rpc {
client: Client,
userpass: Option<(String, String)>,
url: String,
}
impl Rpc { impl Rpc {
pub fn new(daemon: String) -> Rpc { /// Create a new RPC connection.
Rpc(daemon) /// A daemon requiring authentication can be used via including the username and password in the
/// URL.
pub fn new(mut url: String) -> Result<Rpc, RpcError> {
// Parse out the username and password
let userpass = if url.contains('@') {
let url_clone = url.clone();
let split_url = url_clone.split('@').collect::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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. /// Perform a RPC call to the specified method with the provided parameters.
@ -87,8 +124,7 @@ impl Rpc {
method: &str, method: &str,
params: Option<Params>, params: Option<Params>,
) -> Result<Response, RpcError> { ) -> Result<Response, RpcError> {
let client = reqwest::Client::new(); let mut builder = self.client.post(self.url.clone() + "/" + method);
let mut builder = client.post(self.0.clone() + "/" + method);
if let Some(params) = params.as_ref() { if let Some(params) = params.as_ref() {
builder = builder.json(params); builder = builder.json(params);
} }
@ -115,16 +151,35 @@ impl Rpc {
method: &str, method: &str,
params: Vec<u8>, params: Vec<u8>,
) -> Result<Response, RpcError> { ) -> Result<Response, RpcError> {
let client = reqwest::Client::new(); let builder = self.client.post(self.url.clone() + "/" + method).body(params.clone());
let builder = client.post(self.0.clone() + "/" + method).body(params);
self.call_tail(method, builder.header("Content-Type", "application/octet-stream")).await self.call_tail(method, builder.header("Content-Type", "application/octet-stream")).await
} }
async fn call_tail<Response: DeserializeOwned + Debug>( async fn call_tail<Response: DeserializeOwned + Debug>(
&self, &self,
method: &str, method: &str,
builder: reqwest::RequestBuilder, mut builder: RequestBuilder,
) -> Result<Response, RpcError> { ) -> Result<Response, RpcError> {
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)?; let res = builder.send().await.map_err(|_| RpcError::ConnectionError)?;
Ok(if !method.ends_with(".bin") { Ok(if !method.ends_with(".bin") {

View file

@ -11,7 +11,7 @@ use monero_serai::{
}; };
pub async fn rpc() -> Rpc { 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 // Only run once
if rpc.get_height().await.unwrap() != 1 { if rpc.get_height().await.unwrap() != 1 {

View file

@ -70,7 +70,7 @@ pub struct Monero {
impl Monero { impl Monero {
pub async fn new(url: String) -> Monero { pub async fn new(url: String) -> Monero {
Monero { rpc: Rpc::new(url), view: additional_key::<Monero>(0).0 } Monero { rpc: Rpc::new(url).unwrap(), view: additional_key::<Monero>(0).0 }
} }
fn scanner(&self, spend: dfg::EdwardsPoint) -> Scanner { fn scanner(&self, spend: dfg::EdwardsPoint) -> Scanner {