From 42b3df9389e08893c2c0daf974d947b19b0ea173 Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Fri, 3 Dec 2021 18:05:51 +0530 Subject: [PATCH 1/3] Add Socks5 proxy transport for RPC The `ProxyTransport` implements a socks5 stream connection with the core rpc host, and request/receive http data from it. `Transport` trait is implemented on `ProxyTransport` to make it compatible with existing `bitcoincore-rpc::Client`. --- Cargo.toml | 2 +- src/blockchain/rpc_proxy.rs | 241 ++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/blockchain/rpc_proxy.rs diff --git a/Cargo.toml b/Cargo.toml index f97f3f13b..3ca745cf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ compact_filters = ["rocksdb", "socks", "lazy_static", "cc"] key-value-db = ["sled"] all-keys = ["keys-bip39"] keys-bip39 = ["bip39"] -rpc = ["bitcoincore-rpc"] +rpc = ["bitcoincore-rpc", "socks"] # We currently provide mulitple implementations of `Blockchain`, all are # blocking except for the `EsploraBlockchain` which can be either async or diff --git a/src/blockchain/rpc_proxy.rs b/src/blockchain/rpc_proxy.rs new file mode 100644 index 000000000..5645ec477 --- /dev/null +++ b/src/blockchain/rpc_proxy.rs @@ -0,0 +1,241 @@ +// Bitcoin Dev Kit +// Written in 2021 by Rajarshi Maitra +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! A SOCKS5 proxy transport implementation for RPC blockchain. +//! +//! This is currently internal to the lib and only compatible with +//! Bitcoin Core RPC. + +use super::rpc::Auth; +use bitcoin::base64; +use bitcoincore_rpc::jsonrpc::Error as JSONRPC_Error; +use bitcoincore_rpc::jsonrpc::{Request, Response, Transport}; +use bitcoincore_rpc::Error as RPC_Error; +use socks::Socks5Stream; +use std::fs::File; +use std::io::{BufRead, BufReader, Write}; +use std::time::{Duration, Instant}; + +/// Errors that can be thrown by [`ProxyTransport`](crate::blockchain::rpc_proxy::ProxyTransport) +#[derive(Debug)] +pub enum RpcProxyError { + /// Bitcoin core rpc error + CoreRpc(bitcoincore_rpc::Error), + /// IO error + Io(std::io::Error), + /// Invalid RPC url + InvalidUrl, + /// Error serializing or deserializing JSON data + Json(serde_json::Error), + /// RPC timeout error + RpcTimeout, + /// Http Parsing Error + HttpParsing, + /// Http Timeout Error + HttpTimeout, + /// Http Response Code + HttpResponseCode(u16), +} + +impl std::fmt::Display for RpcProxyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl std::error::Error for RpcProxyError {} + +impl_error!(bitcoincore_rpc::Error, CoreRpc, RpcProxyError); +impl_error!(std::io::Error, Io, RpcProxyError); +impl_error!(serde_json::Error, Json, RpcProxyError); + +// We need to backport RpcProxyError to satisfy Transport trait bound +impl From for JSONRPC_Error { + fn from(e: RpcProxyError) -> Self { + Self::Transport(Box::new(e)) + } +} + +/// SOCKS5 proxy transport +/// This is currently designed to work only with Bitcoin Core RPC +pub(crate) struct ProxyTransport { + proxy_addr: String, + target_addr: String, + proxy_credential: Option<(String, String)>, + wallet_path: String, + rpc_auth: Option, + timeout: Duration, +} + +impl ProxyTransport { + /// Create a new ProxyTransport + pub(crate) fn new( + proxy_addr: &str, + rpc_url: &str, + proxy_credential: Option<(String, String)>, + rpc_auth: &Auth, + ) -> Result { + // Fetch the RPC address:port and wallet path from url + let (target_addr, wallet_path) = { + // the url will be of form "http://:/wallet/" + (rpc_url[7..22].to_owned(), rpc_url[22..].to_owned()) + }; + + // fetch username password from rpc authentication + let rpc_auth = { + if let (Some(user), Some(pass)) = Self::get_user_pass(rpc_auth)? { + let mut auth = user; + auth.push(':'); + auth.push_str(&pass[..]); + Some(format!("Basic {}", &base64::encode(auth.as_bytes()))) + } else { + None + } + }; + + Ok(ProxyTransport { + proxy_addr: proxy_addr.to_owned(), + target_addr, + proxy_credential, + wallet_path, + rpc_auth, + timeout: Duration::from_secs(15), // Same as regular RPC default + }) + } + + // Helper function to parse username:password pair for rpc Auth + fn get_user_pass(auth: &Auth) -> Result<(Option, Option), RpcProxyError> { + use std::io::Read; + match auth { + Auth::None => Ok((None, None)), + Auth::UserPass { username, password } => { + Ok((Some(username.clone()), Some(password.clone()))) + } + Auth::Cookie { file } => { + let mut file = File::open(file)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let mut split = contents.splitn(2, ':'); + Ok(( + Some(split.next().ok_or(RPC_Error::InvalidCookieFile)?.into()), + Some(split.next().ok_or(RPC_Error::InvalidCookieFile)?.into()), + )) + } + } + } + + // Try to read a line from a buffered reader. If no line can be read till the deadline is reached + // return a timeout error. + fn get_line(reader: &mut R, deadline: Instant) -> Result { + let mut line = String::new(); + while deadline > Instant::now() { + match reader.read_line(&mut line) { + // EOF reached for now, try again later + Ok(0) => std::thread::sleep(Duration::from_millis(5)), + // received useful data, return it + Ok(_) => return Ok(line), + // io error occurred, abort + Err(e) => return Err(e.into()), + } + } + Err(RpcProxyError::RpcTimeout) + } + + // Http request and response over SOCKS5 + fn request(&self, req: impl serde::Serialize) -> Result + where + R: for<'a> serde::de::Deserialize<'a>, + { + let request_deadline = Instant::now() + self.timeout; + + // Open connection + let mut socks_stream = if let Some((username, password)) = &self.proxy_credential { + Socks5Stream::connect_with_password( + &self.proxy_addr[..], + &self.target_addr[..], + &username[..], + &password[..], + )? + } else { + Socks5Stream::connect(&self.proxy_addr[..], &self.target_addr[..])? + }; + + let socks_stream = socks_stream.get_mut(); + + // Serialize the body first so we can set the Content-Length header. + let body = serde_json::to_vec(&req)?; + + // Send HTTP request + socks_stream.write_all(b"POST ")?; + socks_stream.write_all(self.wallet_path.as_bytes())?; + socks_stream.write_all(b" HTTP/1.1\r\n")?; + // Write headers + socks_stream.write_all(b"Content-Type: application/json-rpc\r\n")?; + socks_stream.write_all(b"Content-Length: ")?; + socks_stream.write_all(body.len().to_string().as_bytes())?; + socks_stream.write_all(b"\r\n")?; + if let Some(ref auth) = self.rpc_auth { + socks_stream.write_all(b"Authorization: ")?; + socks_stream.write_all(auth.as_ref())?; + socks_stream.write_all(b"\r\n")?; + } + // Write body + socks_stream.write_all(b"\r\n")?; + socks_stream.write_all(&body)?; + socks_stream.flush()?; + + // Receive response + let mut reader = BufReader::new(socks_stream); + + // Parse first HTTP response header line + let http_response = Self::get_line(&mut reader, request_deadline)?; + if http_response.len() < 12 || !http_response.starts_with("HTTP/1.1 ") { + return Err(RpcProxyError::HttpParsing); + } + let response_code = match http_response[9..12].parse::() { + Ok(n) => n, + Err(_) => return Err(RpcProxyError::HttpParsing), + }; + + // Skip response header fields + while Self::get_line(&mut reader, request_deadline)? != "\r\n" {} + + // Even if it's != 200, we parse the response as we may get a JSONRPC error instead + // of the less meaningful HTTP error code. + let resp_body = Self::get_line(&mut reader, request_deadline)?; + match serde_json::from_str(&resp_body) { + Ok(s) => Ok(s), + Err(e) => { + if response_code != 200 { + Err(RpcProxyError::HttpResponseCode(response_code)) + } else { + // If it was 200 then probably it was legitimately a parse error + Err(e.into()) + } + } + } + } +} + +// Make ProxyTransport usable in bitcoin_core::rpc::Client +impl Transport for ProxyTransport { + fn send_request(&self, req: Request) -> Result { + Ok(self.request(req)?) + } + + fn send_batch(&self, reqs: &[Request]) -> Result, JSONRPC_Error> { + Ok(self.request(reqs)?) + } + + fn fmt_target(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}/{}", self.target_addr, self.wallet_path) + } +} From a9ce6a37e34c25f80a3d181555745cba4a7006fb Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Fri, 3 Dec 2021 18:10:25 +0530 Subject: [PATCH 2/3] Add RPC proxy options to RpcConfig Handles proxy options to create a socks5 stream through proxy to rpc host. Creates regular http connection otherwise. --- src/blockchain/mod.rs | 3 +++ src/blockchain/rpc.rs | 25 +++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index bbf0303df..fad7746e9 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -60,6 +60,9 @@ pub use self::rpc::RpcBlockchain; #[cfg(feature = "rpc")] pub use self::rpc::RpcConfig; +#[cfg(feature = "rpc")] +pub mod rpc_proxy; + #[cfg(feature = "esplora")] #[cfg_attr(docsrs, doc(cfg(feature = "esplora")))] pub mod esplora; diff --git a/src/blockchain/rpc.rs b/src/blockchain/rpc.rs index 5c9ee2dd7..a92f103ee 100644 --- a/src/blockchain/rpc.rs +++ b/src/blockchain/rpc.rs @@ -24,6 +24,8 @@ //! auth: Auth::Cookie { //! file: "/home/user/.bitcoin/.cookie".into(), //! }, +//! proxy: None, +//! proxy_auth: None, //! network: bdk::bitcoin::Network::Testnet, //! wallet_name: "wallet_name".to_string(), //! skip_blocks: None, @@ -31,6 +33,7 @@ //! let blockchain = RpcBlockchain::from_config(&config); //! ``` +use super::rpc_proxy::ProxyTransport; use crate::bitcoin::consensus::deserialize; use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid}; use crate::blockchain::{Blockchain, Capability, ConfigurableBlockchain, Progress}; @@ -60,7 +63,6 @@ pub struct RpcBlockchain { capabilities: HashSet, /// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block skip_blocks: Option, - /// This is a fixed Address used as a hack key to store information on the node _storage_address: Address, } @@ -72,6 +74,10 @@ pub struct RpcConfig { pub url: String, /// The bitcoin node authentication mechanism pub auth: Auth, + /// A proxy (like Tor) can be used to connect Bitcoin Core RPC + pub proxy: Option, + /// Authentication for proxy server + pub proxy_auth: Option<(String, String)>, /// The network we are using (it will be checked the bitcoin node network matches this) pub network: Network, /// The wallet name in the bitcoin node, consider using [crate::wallet::wallet_name_from_descriptor] for this @@ -358,7 +364,20 @@ impl ConfigurableBlockchain for RpcBlockchain { let wallet_url = format!("{}/wallet/{}", config.url, &wallet_name); debug!("connecting to {} auth:{:?}", wallet_url, config.auth); - let client = Client::new(wallet_url.as_str(), config.auth.clone().into())?; + let client = if let Some(proxy) = &config.proxy { + let proxy_transport = ProxyTransport::new( + proxy, + wallet_url.as_str(), + config.proxy_auth.clone(), + &config.auth, + )?; + Client::from_jsonrpc(bitcoincore_rpc::jsonrpc::Client::with_transport( + proxy_transport, + )) + } else { + Client::new(wallet_url.as_str(), config.auth.clone().into())? + }; + let loaded_wallets = client.list_wallets()?; if loaded_wallets.contains(&wallet_name) { debug!("wallet already loaded {:?}", wallet_name); @@ -437,6 +456,8 @@ crate::bdk_blockchain_tests! { let config = RpcConfig { url: test_client.bitcoind.rpc_url(), auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() }, + proxy: None, + proxy_auth: None, network: Network::Regtest, wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ), skip_blocks: None, From b99f1cf6309c3daa6b7d432ecd77bbc69862af44 Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Fri, 3 Dec 2021 18:11:56 +0530 Subject: [PATCH 3/3] Add ProxyTransport error into Global Error. --- src/error.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/error.rs b/src/error.rs index 15ec5713d..d2da20da1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -139,7 +139,10 @@ pub enum Error { Sled(sled::Error), #[cfg(feature = "rpc")] /// Rpc client error - Rpc(bitcoincore_rpc::Error), + RpcClient(bitcoincore_rpc::Error), + /// Rpc proxy error + #[cfg(feature = "rpc")] + RpcProxy(crate::blockchain::rpc_proxy::RpcProxyError), #[cfg(feature = "sqlite")] /// Rusqlite client error Rusqlite(rusqlite::Error), @@ -196,7 +199,9 @@ impl_error!(electrum_client::Error, Electrum); #[cfg(feature = "key-value-db")] impl_error!(sled::Error, Sled); #[cfg(feature = "rpc")] -impl_error!(bitcoincore_rpc::Error, Rpc); +impl_error!(bitcoincore_rpc::Error, RpcClient); +#[cfg(feature = "rpc")] +impl_error!(crate::blockchain::rpc_proxy::RpcProxyError, RpcProxy); #[cfg(feature = "sqlite")] impl_error!(rusqlite::Error, Rusqlite);