-
Notifications
You must be signed in to change notification settings - Fork 98
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
feat(rpc): Cookie auth system for the RPC endpoint #8900
base: main
Are you sure you want to change the base?
Changes from all commits
8b49c18
045048e
212d517
e91b139
efdbd2a
7a7c14a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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. | ||||||||||
Comment on lines
+318
to
+319
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional
Suggested change
|
||||||||||
fn unauthenticated(&self) -> Result<()>; | ||||||||||
} | ||||||||||
|
||||||||||
/// RPC method implementations. | ||||||||||
|
@@ -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`, | ||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,47 @@ | ||||||
//! Cookie-based authentication for the RPC server. | ||||||
|
||||||
use base64::{engine::general_purpose::URL_SAFE, Engine as _}; | ||||||
use rand::RngCore; | ||||||
|
||||||
use std::{ | ||||||
fs::{remove_file, File}, | ||||||
io::{Read, Write}, | ||||||
path::PathBuf, | ||||||
}; | ||||||
|
||||||
/// 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 */ | ||||||
pub const COOKIEAUTH_FILE: &str = ".cookie"; | ||||||
|
||||||
/// Generate a new auth cookie and return the encoded password. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
pub fn generate(cookie_dir: PathBuf) -> Option<()> { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OptionalReturning |
||||||
let mut data = [0u8; 32]; | ||||||
rand::thread_rng().fill_bytes(&mut data); | ||||||
let encoded_password = URL_SAFE.encode(data); | ||||||
let cookie_content = format!("{}:{}", COOKIEAUTH_USER, encoded_password); | ||||||
|
||||||
let mut file = File::create(cookie_dir.join(COOKIEAUTH_FILE)).ok()?; | ||||||
file.write_all(cookie_content.as_bytes()).ok()?; | ||||||
|
||||||
tracing::info!("RPC auth cookie generated successfully"); | ||||||
|
||||||
Some(()) | ||||||
} | ||||||
|
||||||
/// Get the encoded password from the auth cookie. | ||||||
pub fn get(cookie_dir: PathBuf) -> Option<String> { | ||||||
let mut file = File::open(cookie_dir.join(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<()> { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OptionalSimilarly here. |
||||||
remove_file(COOKIEAUTH_FILE).ok()?; | ||||||
tracing::info!("RPC auth cookie deleted successfully"); | ||||||
Some(()) | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,15 @@ | |
//! | ||
//! These fixes are applied at the HTTP level, before the RPC request is parsed. | ||
|
||
use base64::{engine::general_purpose::URL_SAFE, Engine as _}; | ||
use futures::TryStreamExt; | ||
use jsonrpc_http_server::{ | ||
hyper::{body::Bytes, header, Body, Request}, | ||
RequestMiddleware, RequestMiddlewareAction, | ||
}; | ||
|
||
use crate::server::cookie; | ||
|
||
/// HTTP [`RequestMiddleware`] with compatibility workarounds. | ||
/// | ||
/// This middleware makes the following changes to HTTP requests: | ||
|
@@ -34,13 +37,18 @@ 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(pub crate::config::Config); | ||
|
||
impl RequestMiddleware for FixHttpRequestMiddleware { | ||
fn on_request(&self, mut request: Request<Body>) -> RequestMiddlewareAction { | ||
tracing::trace!(?request, "original HTTP request"); | ||
|
||
// Check if the request is authenticated | ||
if !self.check_credentials(request.headers_mut()) { | ||
request = Self::unauthenticated(request); | ||
} | ||
|
||
// Fix the request headers if needed and we can do so. | ||
FixHttpRequestMiddleware::insert_or_replace_content_type_header(request.headers_mut()); | ||
|
||
|
@@ -141,4 +149,51 @@ impl FixHttpRequestMiddleware { | |
); | ||
} | ||
} | ||
|
||
/// Change the method name in the JSON request. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OptionalCould we move the contents of this fn to |
||
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) | ||
}) | ||
} | ||
|
||
/// Check if the request is authenticated. | ||
pub fn check_credentials(&self, headers: &header::HeaderMap) -> bool { | ||
headers | ||
.get(header::AUTHORIZATION) | ||
.and_then(|auth_header| auth_header.to_str().ok()) | ||
.and_then(|auth| auth.split_whitespace().nth(1)) | ||
.and_then(|token| URL_SAFE.decode(token).ok()) | ||
.and_then(|decoded| String::from_utf8(decoded).ok()) | ||
.and_then(|decoded_str| { | ||
decoded_str | ||
.split(':') | ||
.nth(1) | ||
.map(|password| password.to_string()) | ||
}) | ||
.map_or(false, |password| { | ||
if let Some(cookie_password) = cookie::get(self.0.cookie_dir.clone()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We read the cookie from the disk on each check here and only rely on the OS caching the disk reads. We could keep the cookie in the memory instead. For example, by using |
||
cookie_password == password | ||
} else { | ||
false | ||
} | ||
}) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.