Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RPC authentication #86

Closed
wants to merge 9 commits into from
26 changes: 5 additions & 21 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,14 @@ jobs:
matrix:
monero: [ 0.17.3.0, 0.17.3.2, 0.18.0.0 ]

services:
monerod:
image: ghcr.io/farcaster-project/containers/monerod:${{ matrix.monero }}
env:
NETWORK: regtest
MONEROD_RPC_PORT: 18081
MONEROD_ZMQ_PORT: 18082
OFFLINE: --offline
DIFFICULTY: 1
ports:
- 18081:18081
- 18082:18082
monero-wallet-rpc:
image: ghcr.io/farcaster-project/containers/monero-wallet-rpc:${{ matrix.monero }}
env:
MONERO_DAEMON_ADDRESS: monerod:18081
MONERO_DAEMON_HOST: monerod:18081
WALLET_RPC_PORT: 18083
ports:
- 18083:18083

steps:
- uses: actions/checkout@v3

- name: Spin up containers
run: docker-compose -f tests/docker-compose.yml up -d
env:
MONERO_VERSION: ${{ matrix.monero }}

- name: Install Rust Stable
uses: actions-rs/toolchain@v1
with:
Expand Down
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ description = "RPC client for Monero daemon and wallet"
[dependencies]
anyhow = "1"
chrono = { version = "0.4", features = ["serde"] }
fixed-hash = "0.7"
diqwest = { version = "1.1", optional = true }
fixed-hash = "0.8"
hex = "0.4"
http = "0.2"
jsonrpc-core = "18"
Expand All @@ -30,3 +31,6 @@ rand = "0.8.4"
rustc-hex = "2.1"
serde_test = "1.0"
tokio = { version = "1.12.0", features = ["full"] }

[features]
rpc_authentication = ["dep:diqwest"]
66 changes: 52 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,23 @@ use std::{
use tracing::*;
use uuid::Uuid;

#[cfg(feature = "rpc_authentication")]
use crate::RpcAuthentication::Credentials;
#[cfg(feature = "rpc_authentication")]
use diqwest::WithDigestAuth;

enum RpcParams {
Array(Box<dyn Iterator<Item = Value> + Send + 'static>),
Map(Box<dyn Iterator<Item = (String, Value)> + Send + 'static>),
None,
}

#[derive(Clone, Debug)]
pub enum RpcAuthentication {
Credentials { username: String, password: String },
None,
}

impl RpcParams {
fn array<A>(v: A) -> Self
where
Expand Down Expand Up @@ -97,6 +108,8 @@ impl From<RpcParams> for Params {
struct RemoteCaller {
http_client: reqwest::Client,
addr: String,
#[allow(dead_code)]
rpc_auth: RpcAuthentication,
}

impl RemoteCaller {
Expand All @@ -117,13 +130,20 @@ impl RemoteCaller {

trace!("Sending JSON-RPC method call: {:?}", method_call);

let rsp = client
.post(&uri)
.json(&method_call)
.send()
.await?
.json::<response::Output>()
.await?;
let req = client.post(&uri).json(&method_call);

#[cfg(not(feature = "rpc_authentication"))]
let rsp = req.send().await?.json::<response::Output>().await?;

#[cfg(feature = "rpc_authentication")]
let rsp = if let Credentials { username, password } = &self.rpc_auth {
req.send_with_digest_auth(username, password)
.await?
.json::<response::Output>()
.await?
} else {
req.send().await?.json::<response::Output>().await?
};

trace!("Received JSON-RPC response: {:?}", rsp);
let v = jsonrpc_core::Result::<Value>::from(rsp);
Expand All @@ -145,13 +165,20 @@ impl RemoteCaller {
json_params
);

let rsp = client
.post(uri)
.json(&json_params)
.send()
.await?
.json::<T>()
.await?;
let req = client.post(uri).json(&json_params);

#[cfg(not(feature = "rpc_authentication"))]
let rsp = req.send().await?.json::<T>().await?;

#[cfg(feature = "rpc_authentication")]
let rsp = if let Credentials { username, password } = &self.rpc_auth {
req.send_with_digest_auth(username, password)
.await?
.json::<T>()
.await?
} else {
req.send().await?.json::<T>().await?
};

trace!("Received daemon RPC response: {:?}", rsp);

Expand Down Expand Up @@ -198,6 +225,17 @@ impl RpcClient {
inner: CallerWrapper(Arc::new(RemoteCaller {
http_client: reqwest::ClientBuilder::new().build().unwrap(),
addr,
rpc_auth: RpcAuthentication::None,
})),
}
}

pub fn with_authentication(addr: String, rpc_auth: RpcAuthentication) -> Self {
Self {
inner: CallerWrapper(Arc::new(RemoteCaller {
http_client: reqwest::ClientBuilder::new().build().unwrap(),
addr,
rpc_auth,
})),
}
}
Expand Down
38 changes: 36 additions & 2 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,54 @@
version: "3.7"
services:
monerod:
image: ghcr.io/farcaster-project/containers/monerod:0.18.0.0
image: ghcr.io/farcaster-project/containers/monerod:${MONERO_VERSION:-0.18.0.0}
environment:
NETWORK: regtest
MONEROD_RPC_PORT: 18081
MONEROD_ZMQ_PORT: 18082
OFFLINE: --offline
DIFFICULTY: 1
ports:
- 18081:18081

monero-wallet-rpc:
image: ghcr.io/farcaster-project/containers/monero-wallet-rpc:0.18.0.0
image: ghcr.io/farcaster-project/containers/monero-wallet-rpc:${MONERO_VERSION:-0.18.0.0}
environment:
MONERO_DAEMON_ADDRESS: monerod:18081
MONERO_DAEMON_HOST: monerod:18081
WALLET_RPC_PORT: 18083
depends_on:
- "monerod"
ports:
- 18083:18083

monerod-rpc-authentication:
image: ghcr.io/farcaster-project/containers/monerod:${MONERO_VERSION:-0.18.0.0}
depends_on:
- "monerod"
ports:
- 18085:18081
command: >
monerod --regtest
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 18081
--rpc-login foo:bar
--confirm-external-bind
--non-interactive
--offline
--fixed-difficulty 1

monero-wallet-rpc-authentication:
image: ghcr.io/farcaster-project/containers/monero-wallet-rpc:${MONERO_VERSION:-0.18.0.0}
depends_on:
- "monerod"
ports:
- 18084:18084
command: >
monero-wallet-rpc --rpc-login foo:bar
--wallet-dir wallets
--daemon-address monerod:18081
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 18084
--confirm-external-bind
--trusted-daemon
65 changes: 65 additions & 0 deletions tests/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use monero::Hash;
use monero_rpc::{RpcAuthentication, RpcClient};
use std::env;

mod clients_tests;

#[tokio::test]
Expand Down Expand Up @@ -58,3 +62,64 @@ async fn main_functional_test() {

clients_tests::all_clients_interaction::run().await;
}

#[cfg(feature = "rpc_authentication")]
fn setup_rpc_auth_client(username: &str, password: &str, port: u32) -> RpcClient {
let whost = env::var("MONERO_WALLET_HOST_1").unwrap_or_else(|_| "localhost".into());
let rpc_credentials = RpcAuthentication::Credentials {
username: username.into(),
password: password.into(),
};
let rpc_client =
RpcClient::with_authentication(format!("http://{}:{}", whost, port), rpc_credentials);

rpc_client
}

#[tokio::test]
#[cfg(feature = "rpc_authentication")]
async fn test_daemon_rpc_auth() {
let rpc_client = setup_rpc_auth_client("foo", "bar", 18085).daemon();
let daemon_transactions = rpc_client.get_block_count().await;

assert!(daemon_transactions.is_ok());
}

#[tokio::test]
#[cfg(feature = "rpc_authentication")]
async fn test_daemon_rpc_auth_fail() {
let rpc_client = setup_rpc_auth_client("invalid", "bar", 18085).daemon();
let daemon_transactions = rpc_client.get_block_count().await;

assert!(daemon_transactions.is_err());
}

#[tokio::test]
#[cfg(feature = "rpc_authentication")]
async fn test_daemon_rpc_rpc_auth() {
let rpc_client = setup_rpc_auth_client("foo", "bar", 18085).daemon_rpc();
let transactions = vec![Hash::from_low_u64_be(1)];
let daemon_transactions = rpc_client
.get_transactions(transactions, Some(true), Some(true))
.await;

assert!(daemon_transactions.is_ok());
}

#[tokio::test]
#[cfg(feature = "rpc_authentication")]
async fn test_rpc_auth() {
let rpc_client = setup_rpc_auth_client("foo", "bar", 18084).wallet();
assert!(rpc_client.get_version().await.is_ok());

let version = rpc_client.get_version().await.unwrap();
assert!(version.0 > 0);
}

#[tokio::test]
#[cfg(feature = "rpc_authentication")]
async fn test_rpc_auth_fail() {
let rpc_client = setup_rpc_auth_client("invalid", "auth", 18084).wallet();

assert!(rpc_client.get_version().await.is_err());
}