Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ clap = { version = "4.5.6", features = ["derive", "env"] }
log = "0.4.21"
serde = { version = "1.0.200", features = ["derive"] }
hex = "0.4.3"
rand = "0.8"
jsonrpsee = { version = "0.22.5", features = ["server", "http-client", "macros"] }
directories = "5.0.1"
env_logger = "0.11.3"
Expand All @@ -40,6 +41,7 @@ tabled = "0.17.0"
colored = "3.0.0"
domain = {version = "0.10.3", default-features = false, features = ["zonefile"]}
tower = "0.4.13"
hyper = "0.14.28"

[dev-dependencies]
assert_cmd = "2.0.16"
Expand Down
3 changes: 2 additions & 1 deletion client/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,12 @@ impl App {
let rpc_server = RpcServerImpl::new(async_chain_state.clone(), wallet_manager);

let bind = spaced.bind.clone();
let auth_token = spaced.auth_token.clone();
let shutdown = self.shutdown.clone();

self.services.spawn(async move {
rpc_server
.listen(bind, shutdown)
.listen(bind, auth_token, shutdown)
.await
.map_err(|e| anyhow!("RPC Server error: {}", e))
});
Expand Down
114 changes: 114 additions & 0 deletions client/src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use base64::Engine;
use hyper::{http::HeaderValue, Body, HeaderMap, Request, Response, StatusCode};
use jsonrpsee::{
core::ClientError,
http_client::{HttpClient, HttpClientBuilder},
};
use std::{
error::Error,
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use tower::{Layer, Service};

#[derive(Debug, Clone)]
pub(crate) struct BasicAuthLayer {
token: String,
}

impl BasicAuthLayer {
pub fn new(token: String) -> Self {
Self { token }
}
}

impl<S> Layer<S> for BasicAuthLayer {
type Service = BasicAuth<S>;

fn layer(&self, inner: S) -> Self::Service {
BasicAuth::new(inner, self.token.clone())
}
}

#[derive(Debug, Clone)]
pub(crate) struct BasicAuth<S> {
inner: S,
token: Arc<str>,
}

impl<S> BasicAuth<S> {
pub fn new(inner: S, token: String) -> Self {
Self {
inner,
token: Arc::from(token.as_str()),
}
}

fn check_auth(&self, headers: &HeaderMap) -> bool {
headers
.get("authorization")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.strip_prefix("Basic "))
.map_or(false, |token| token == self.token.as_ref())
}

fn unauthorized_response() -> Response<Body> {
Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header("WWW-Authenticate", "Basic realm=\"Protected\"")
.body(Body::from("Unauthorized"))
.expect("Failed to build unauthorized response")
}
}

impl<S> Service<Request<Body>> for BasicAuth<S>
where
S: Service<Request<Body>, Response = Response<Body>>,
S::Response: 'static,
S::Error: Into<Box<dyn Error + Send + Sync>> + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = Box<dyn Error + Send + Sync + 'static>;
type Future =
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;

#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx).map_err(Into::into)
}

fn call(&mut self, req: Request<Body>) -> Self::Future {
if !self.check_auth(req.headers()) {
let response = Self::unauthorized_response();
return Box::pin(async move { Ok(response) });
}

let fut = self.inner.call(req);
let res_fut = async move { fut.await.map_err(|err| err.into()) };
Box::pin(res_fut)
}
}

pub fn auth_cookie(user: &str, password: &str) -> String {
format!("{user}:{password}")
}

pub fn auth_token_from_cookie(cookie: &str) -> String {
base64::prelude::BASE64_STANDARD.encode(cookie)
}

pub fn auth_token_from_creds(user: &str, password: &str) -> String {
base64::prelude::BASE64_STANDARD.encode(auth_cookie(user, password))
}

pub fn http_client_with_auth(url: &str, auth_token: &str) -> Result<HttpClient, ClientError> {
let mut headers = hyper::http::HeaderMap::new();
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Basic {auth_token}")).unwrap(),
);
HttpClientBuilder::default().set_headers(headers).build(url)
}
41 changes: 35 additions & 6 deletions client/src/bin/space-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ use domain::{
};
use jsonrpsee::{
core::{client::Error, ClientError},
http_client::{HttpClient, HttpClientBuilder},
http_client::HttpClient,
};
use serde::{Deserialize, Serialize};
use spaces_client::{
config::{default_spaces_rpc_port, ExtendedNetwork},
auth::{auth_token_from_cookie, auth_token_from_creds, http_client_with_auth},
config::{default_cookie_path, default_spaces_rpc_port, ExtendedNetwork},
deserialize_base64,
format::{
print_error_rpc_response, print_list_bidouts, print_list_spaces_response,
print_list_transactions, print_list_unspent, print_server_info,
print_list_wallets, print_wallet_balance_response, print_wallet_info, print_wallet_response,
Format,
print_list_transactions, print_list_unspent, print_list_wallets, print_server_info,
print_wallet_balance_response, print_wallet_info, print_wallet_response, Format,
},
rpc::{
BidParams, ExecuteParams, OpenParams, RegisterParams, RpcClient, RpcWalletRequest,
Expand Down Expand Up @@ -54,6 +54,15 @@ pub struct Args {
/// Spaced RPC URL [default: based on specified chain]
#[arg(long)]
spaced_rpc_url: Option<String>,
/// Spaced RPC cookie file path
#[arg(long, env = "SPACED_RPC_COOKIE")]
rpc_cookie: Option<PathBuf>,
/// Spaced RPC user
#[arg(long, requires = "rpc_password", env = "SPACED_RPC_USER")]
rpc_user: Option<String>,
/// Spaced RPC password
#[arg(long, env = "SPACED_RPC_PASSWORD")]
rpc_password: Option<String>,
/// Specify wallet to use
#[arg(long, short, global = true, default_value = "default")]
wallet: String,
Expand Down Expand Up @@ -387,7 +396,27 @@ impl SpaceCli {
args.spaced_rpc_url = Some(default_spaced_rpc_url(&args.chain));
}

let client = HttpClientBuilder::default().build(args.spaced_rpc_url.clone().unwrap())?;
let auth_token = if args.rpc_user.is_some() {
auth_token_from_creds(
args.rpc_user.as_ref().unwrap(),
args.rpc_password.as_ref().unwrap(),
)
} else {
let cookie_path = match &args.rpc_cookie {
Some(path) => path,
None => &default_cookie_path(&args.chain),
};
let cookie = fs::read_to_string(cookie_path).map_err(|e| {
anyhow!(
"Failed to read cookie file '{}': {}",
cookie_path.display(),
e
)
})?;
auth_token_from_cookie(&cookie)
};
let client = http_client_with_auth(args.spaced_rpc_url.as_ref().unwrap(), &auth_token)?;

Ok((
Self {
wallet: args.wallet.clone(),
Expand Down
60 changes: 51 additions & 9 deletions client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,23 @@ use std::{
path::PathBuf,
};

use clap::{
ArgGroup, Parser, ValueEnum,
};
use anyhow::anyhow;
use clap::{ArgGroup, Parser, ValueEnum};
use directories::ProjectDirs;
use jsonrpsee::core::Serialize;
use log::error;
use rand::{
distributions::Alphanumeric,
{thread_rng, Rng},
};
use serde::Deserialize;
use spaces_protocol::bitcoin::Network;

use crate::{
auth::{auth_token_from_cookie, auth_token_from_creds},
source::{BitcoinRpc, BitcoinRpcAuth},
store::{LiveStore, Store},
spaces::Spaced,
store::{LiveStore, Store},
};

const RPC_OPTIONS: &str = "RPC Server Options";
Expand Down Expand Up @@ -58,6 +62,12 @@ pub struct Args {
/// Bitcoin RPC password
#[arg(long, env = "SPACED_BITCOIN_RPC_PASSWORD")]
bitcoin_rpc_password: Option<String>,
/// Spaced RPC user
#[arg(long, requires = "rpc_password", env = "SPACED_RPC_USER")]
rpc_user: Option<String>,
/// Spaced RPC password
#[arg(long, env = "SPACED_RPC_PASSWORD")]
rpc_password: Option<String>,
/// Bind to given address to listen for JSON-RPC connections.
/// This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost)
#[arg(long, help_heading = Some(RPC_OPTIONS), default_values = ["127.0.0.1", "::1"], env = "SPACED_RPC_BIND")]
Expand Down Expand Up @@ -102,7 +112,7 @@ impl Args {
/// Configures spaced node by processing command line arguments
/// and configuration files
pub async fn configure(args: Vec<String>) -> anyhow::Result<Spaced> {
let mut args = Args::try_parse_from(args)?;
let mut args = Args::try_parse_from(args)?;
let default_dirs = get_default_node_dirs();

if args.bitcoin_rpc_url.is_none() {
Expand All @@ -117,6 +127,7 @@ impl Args {
Some(data_dir) => data_dir,
}
.join(args.chain.to_string());
fs::create_dir_all(data_dir.clone())?;

let default_port = args.rpc_port.unwrap();
let rpc_bind_addresses: Vec<SocketAddr> = args
Expand All @@ -132,6 +143,31 @@ impl Args {
})
.collect();

let auth_token = if args.rpc_user.is_some() {
auth_token_from_creds(
args.rpc_user.as_ref().unwrap(),
args.rpc_password.as_ref().unwrap(),
)
} else {
let cookie = format!(
"__cookie__:{}",
thread_rng()
.sample_iter(&Alphanumeric)
.take(64)
.map(char::from)
.collect::<String>()
);
let cookie_path = data_dir.join(".cookie");
fs::write(&cookie_path, &cookie).map_err(|e| {
anyhow!(
"Failed to write cookie file '{}': {}",
cookie_path.display(),
e
)
})?;
auth_token_from_cookie(&cookie)
};

let bitcoin_rpc_auth = if let Some(cookie) = args.bitcoin_rpc_cookie {
let cookie = std::fs::read_to_string(cookie)?;
BitcoinRpcAuth::Cookie(cookie)
Expand All @@ -144,13 +180,11 @@ impl Args {
let rpc = BitcoinRpc::new(
&args.bitcoin_rpc_url.expect("bitcoin rpc url"),
bitcoin_rpc_auth,
!args.bitcoin_rpc_light
!args.bitcoin_rpc_light,
);

let genesis = Spaced::genesis(args.chain);

fs::create_dir_all(data_dir.clone())?;

let proto_db_path = data_dir.join("protocol.sdb");
let initial_sync = !proto_db_path.exists();

Expand Down Expand Up @@ -196,13 +230,14 @@ impl Args {
rpc,
data_dir,
bind: rpc_bind_addresses,
auth_token,
chain,
block_index,
block_index_full: args.block_index_full,
num_workers: args.jobs as usize,
anchors_path,
synced: false,
cbf: args.bitcoin_rpc_light
cbf: args.bitcoin_rpc_light,
})
}
}
Expand All @@ -214,6 +249,13 @@ fn get_default_node_dirs() -> ProjectDirs {
})
}

pub fn default_cookie_path(network: &ExtendedNetwork) -> PathBuf {
get_default_node_dirs()
.data_dir()
.join(network.to_string())
.join(".cookie")
}

// from clap utilities
pub fn safe_exit(code: i32) -> ! {
use std::io::Write;
Expand Down
1 change: 1 addition & 0 deletions client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::time::{Duration, Instant};
use base64::Engine;
use serde::{Deserialize, Deserializer, Serializer};

pub mod auth;
mod checker;
pub mod client;
pub mod config;
Expand Down
Loading
Loading