Skip to content

Commit

Permalink
add a cookie auth system for the rpc endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
oxarbitrage committed Sep 30, 2024
1 parent ac1242a commit 8b49c18
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6164,6 +6164,7 @@ dependencies = [
name = "zebra-rpc"
version = "1.0.0-beta.39"
dependencies = [
"base64 0.22.1",
"chrono",
"futures",
"hex",
Expand Down
3 changes: 3 additions & 0 deletions zebra-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ jsonrpc-http-server = "18.0.0"
serde_json = { version = "1.0.122", features = ["preserve_order"] }
indexmap = { version = "2.3.0", features = ["serde"] }

# RPC endpoint basic auth
base64 = "0.22.1"

tokio = { version = "1.39.2", features = [
"time",
"rt-multi-thread",
Expand Down
12 changes: 12 additions & 0 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ pub trait Rpc {
/// tags: control
#[rpc(name = "stop")]
fn stop(&self) -> Result<String>;

#[rpc(name = "unauthenticated")]
/// A dummy RPC method that just returns a non-authenticated RPC error.
fn unauthenticated(&self) -> Result<()>;
}

/// RPC method implementations.
Expand Down Expand Up @@ -1383,6 +1387,14 @@ where
data: None,
})
}

fn unauthenticated(&self) -> Result<()> {
Err(Error {
code: ErrorCode::ServerError(401),
message: "unauthenticated method".to_string(),
data: None,
})
}
}

/// Returns the best chain tip height of `latest_chain_tip`,
Expand Down
7 changes: 6 additions & 1 deletion zebra-rpc/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use crate::{
#[cfg(feature = "getblocktemplate-rpcs")]
use crate::methods::{GetBlockTemplateRpc, GetBlockTemplateRpcImpl};

mod cookie;
pub mod http_request_compatibility;
pub mod rpc_call_compatibility;

Expand Down Expand Up @@ -193,6 +194,9 @@ impl RpcServer {
parallel_cpu_threads = available_parallelism().map(usize::from).unwrap_or(1);
}

// generate a cookie
let generated_password = cookie::generate().unwrap_or("".to_string());

// The server is a blocking task, which blocks on executor shutdown.
// So we need to start it in a std::thread.
// (Otherwise tokio panics on RPC port conflict, which shuts down the RPC server.)
Expand All @@ -205,7 +209,7 @@ impl RpcServer {
.threads(parallel_cpu_threads)
// TODO: disable this security check if we see errors from lightwalletd
//.allowed_hosts(DomainsValidation::Disabled)
.request_middleware(FixHttpRequestMiddleware)
.request_middleware(FixHttpRequestMiddleware::new(generated_password))
.start_http(&listen_addr)
.expect("Unable to start RPC server");

Expand Down Expand Up @@ -299,6 +303,7 @@ impl RpcServer {
span.in_scope(|| {
info!("Stopping RPC server");
close_handle.clone().close();
cookie::delete(); // delete the auth cookie
debug!("Stopped RPC server");
})
};
Expand Down
46 changes: 46 additions & 0 deletions zebra-rpc/src/server/cookie.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Cookie-based authentication for the RPC server.
use base64::Engine;
use rand::RngCore;

Check failure on line 4 in zebra-rpc/src/server/cookie.rs

View workflow job for this annotation

GitHub Actions / Build zebrad crate

unresolved import `rand`

use std::{
fs::{remove_file, File},
io::{Read, Write},
};

/// The user field in the cookie (arbitrary, only for recognizability in debugging/logging purposes)
pub const COOKIEAUTH_USER: &str = "__cookie__";
/// Default name for auth cookie file */
const COOKIEAUTH_FILE: &str = ".cookie";

/// Generate a new auth cookie and return the encoded password.
pub fn generate() -> Option<String> {
let mut data = [0u8; 32];
rand::thread_rng().fill_bytes(&mut data);

Check failure on line 19 in zebra-rpc/src/server/cookie.rs

View workflow job for this annotation

GitHub Actions / Build zebrad crate

failed to resolve: use of undeclared crate or module `rand`
let encoded_password = base64::prelude::BASE64_STANDARD.encode(data);
let cookie_content = format!("{}:{}", COOKIEAUTH_USER, encoded_password);

let mut file = File::create(COOKIEAUTH_FILE).ok()?;
file.write_all(cookie_content.as_bytes()).ok()?;

tracing::info!("RPC auth cookie generated successfully");

Some(encoded_password)
}

/// Get the encoded password from the auth cookie.
pub fn get() -> Option<String> {
let mut file = File::open(COOKIEAUTH_FILE).ok()?;
let mut contents = String::new();
file.read_to_string(&mut contents).ok()?;

let parts: Vec<&str> = contents.split(":").collect();
Some(parts[1].to_string())
}

/// Delete the auth cookie.
pub fn delete() -> Option<()> {
remove_file(COOKIEAUTH_FILE).ok()?;
tracing::info!("RPC auth cookie deleted successfully");
Some(())
}
49 changes: 46 additions & 3 deletions zebra-rpc/src/server/http_request_compatibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use jsonrpc_http_server::{
RequestMiddleware, RequestMiddlewareAction,
};

use crate::server::cookie;

/// HTTP [`RequestMiddleware`] with compatibility workarounds.
///
/// This middleware makes the following changes to HTTP requests:
Expand All @@ -34,8 +36,8 @@ use jsonrpc_http_server::{
/// Any user-specified data in RPC requests is hex or base58check encoded.
/// We assume lightwalletd validates data encodings before sending it on to Zebra.
/// So any fixes Zebra performs won't change user-specified data.
#[derive(Copy, Clone, Debug)]
pub struct FixHttpRequestMiddleware;
#[derive(Clone, Debug)]
pub struct FixHttpRequestMiddleware(String);

impl RequestMiddleware for FixHttpRequestMiddleware {
fn on_request(&self, mut request: Request<Body>) -> RequestMiddlewareAction {
Expand All @@ -45,7 +47,7 @@ impl RequestMiddleware for FixHttpRequestMiddleware {
FixHttpRequestMiddleware::insert_or_replace_content_type_header(request.headers_mut());

// Fix the request body
let request = request.map(|body| {
let mut request = request.map(|body| {
let body = body.map_ok(|data| {
// To simplify data handling, we assume that any search strings won't be split
// across multiple `Bytes` data buffers.
Expand All @@ -70,6 +72,18 @@ impl RequestMiddleware for FixHttpRequestMiddleware {
Body::wrap_stream(body)
});

// Check if the request is authenticated
match cookie::get() {
Some(password) => {
if password != self.0 {
request = Self::unauthenticated(request);
}
}
None => {
request = Self::unauthenticated(request);
}
}

tracing::trace!(?request, "modified HTTP request");

RequestMiddlewareAction::Proceed {
Expand Down Expand Up @@ -141,4 +155,33 @@ impl FixHttpRequestMiddleware {
);
}
}

/// Create a new `FixHttpRequestMiddleware`.
pub fn new(password: String) -> Self {
Self(password)
}

/// Change the method name in the JSON request.
fn change_method_name(data: String) -> String {
let mut json_data: serde_json::Value = serde_json::from_str(&data).expect("Invalid JSON");

if let Some(method) = json_data.get_mut("method") {
*method = serde_json::json!("unauthenticated");
}

serde_json::to_string(&json_data).expect("Failed to serialize JSON")
}

/// Modify the request name to be `unauthenticated`.
fn unauthenticated(request: Request<Body>) -> Request<Body> {
request.map(|body| {
let body = body.map_ok(|data| {
let mut data = String::from_utf8_lossy(data.as_ref()).to_string();
data = Self::change_method_name(data);
Bytes::from(data)
});

Body::wrap_stream(body)
})
}
}

0 comments on commit 8b49c18

Please sign in to comment.