// Copyright 2021 The Grin Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Controller for wallet, instantiates and handles listeners (or single-run //! invocations) as needed. use crate::api::{self, ApiServer, BasicAuthMiddleware, ResponseFuture, Router, TLSConfig}; use crate::config::TorConfig; use crate::impls::tor::config as tor_config; use crate::impls::tor::process as tor_process; use crate::impls::tor::{bridge as tor_bridge, proxy as tor_proxy}; use crate::keychain::Keychain; use crate::libwallet::{ address, Error, NodeClient, NodeVersionInfo, Slate, SlatepackAddress, WalletInst, WalletLCProvider, GRIN_BLOCK_HEADER_VERSION, }; use crate::util::secp::key::SecretKey; use crate::util::{from_hex, static_secp_instance, to_base64, Mutex}; use grin_wallet_api::JsonId; use grin_wallet_util::OnionV3Address; use hyper::header::HeaderValue; use hyper::{Request, Response, StatusCode}; use qr_code::QrCode; use serde::{Deserialize, Serialize}; use serde_json; use std::collections::HashMap; use std::convert::TryFrom; use std::io::Cursor; use std::net::SocketAddr; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; use crate::apiwallet::{ ConfigPath, EncryptedRequest, EncryptedResponse, EncryptionErrorResponse, Foreign, ForeignCheckMiddlewareFn, ForeignRpc, Owner, OwnerRpc, }; use easy_jsonrpc_mw; use easy_jsonrpc_mw::{Handler, MaybeReply}; use grin_api::ApiBody; use grin_wallet_config::config::{ add_global_config_listener, get_global_config, remove_global_config_listener, }; use grin_wallet_impls::tor::arti::{start_tor_service, stop_tor_service}; use grin_wallet_impls::tor::process::TorProcess; use http_body_util::{BodyExt, Full}; use hyper::body::Incoming; use tokio::sync::mpsc; lazy_static! { pub static ref GRIN_OWNER_BASIC_REALM: HeaderValue = HeaderValue::from_str("Basic realm=GrinOwnerAPI").unwrap(); } fn check_middleware( name: ForeignCheckMiddlewareFn, node_version_info: Option, slate: Option<&Slate>, ) -> Result<(), Error> { match name { // allow coinbases to be built regardless ForeignCheckMiddlewareFn::BuildCoinbase => Ok(()), _ => { let mut bhv = 3; if let Some(n) = node_version_info { bhv = n.block_header_version; } if let Some(s) = slate { if bhv > 4 && s.version_info.block_header_version < GRIN_BLOCK_HEADER_VERSION { Err(Error::Compatibility( "Incoming Slate is not compatible with this wallet. \ Please upgrade the node or use a different one." .into(), ))?; } } Ok(()) } } } /// initiate the tor listener fn init_tor_listener( sec_key: SecretKey, tor_dir: String, addr: &str, tor_config: TorConfig, ) -> Result { info!("Starting external Tor Process listener."); let mut process = tor_process::TorProcess::new(); let tor_timeout = tor_config.bootstrap_timeout().as_secs(); let mut hm_tor_bridge: HashMap = HashMap::new(); if tor_config.bridge.bridge_line.is_some() { let bridge_config = tor_bridge::TorBridge::try_from(tor_config.bridge) .map_err(|e| Error::TorConfig(format!("{}", e).into()))?; hm_tor_bridge = bridge_config .to_hashmap() .map_err(|e| Error::TorConfig(format!("{}", e).into()))?; } let mut hm_tor_poxy: HashMap = HashMap::new(); if tor_config.proxy.transport.is_some() || tor_config.proxy.allowed_port.is_some() { let proxy_config = tor_proxy::TorProxy::try_from(tor_config.proxy) .map_err(|e| Error::TorConfig(format!("{}", e).into()))?; hm_tor_poxy = proxy_config .to_hashmap() .map_err(|e| Error::TorConfig(format!("{}", e).into()))?; } tor_config::output_tor_listener_config( &tor_dir, addr, &vec![sec_key], hm_tor_bridge, hm_tor_poxy, ) .map_err(|e| Error::TorConfig(format!("{:?}", e).into()))?; // Start TOR process process .torrc_path(&format!("{}/torrc", tor_dir)) .working_dir(&tor_dir) .timeout(tor_timeout) .completion_percent(100) .launch() .map_err(|e| Error::TorProcess(format!("{:?}", e)))?; Ok(process) } /// Instantiate wallet Owner API for a single-use (command line) call /// Return a function containing a loaded API context to call pub fn owner_single_use( wallet: Arc>>>, keychain_mask: Option<&SecretKey>, config_path: PathBuf, f: F, ) -> Result<(), Error> where L: WalletLCProvider<'static, C, K> + 'static, F: FnOnce(&mut Owner, Option<&SecretKey>) -> Result<(), Error>, C: NodeClient + 'static, K: Keychain + 'static, { f(&mut Owner::new(wallet, None, config_path), keychain_mask) } /// Instantiate wallet Foreign API for a single-use (command line) call /// Return a function containing a loaded API context to call pub fn foreign_single_use<'a, L, F, C, K>( wallet: Arc>>>, config_path: PathBuf, keychain_mask: Option, f: F, ) -> Result<(), Error> where L: WalletLCProvider<'a, C, K>, F: FnOnce(&mut Foreign<'a, L, C, K>) -> Result<(), Error>, C: NodeClient + 'a, K: Keychain + 'a, { f(&mut Foreign::new( wallet, config_path, keychain_mask, Some(check_middleware), false, ))?; Ok(()) } /// Listener version, providing same API but listening for requests on a /// port and wrapping the calls /// Note keychain mask is only provided here in case the foreign listener is also being used /// in the same wallet instance pub fn owner_listener( owner_api: &mut Owner, keychain_mask: Arc>>, addr: &str, api_secret: Option, tls_config: Option, owner_api_include_foreign: Option, test_mode: bool, ) -> Result<(), Error> where L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: Keychain + 'static, { let mut router = Router::new(); if api_secret.is_some() { let api_basic_auth = "Basic ".to_string() + &to_base64(&("grin:".to_string() + &api_secret.unwrap())); let basic_auth_middleware = Arc::new(BasicAuthMiddleware::new( api_basic_auth, &GRIN_OWNER_BASIC_REALM, Some("/v2/foreign".into()), )); router.add_middleware(basic_auth_middleware); } let mut running_foreign = false; if owner_api_include_foreign.unwrap_or(false) { running_foreign = true; } let wallet = owner_api.wallet_inst.clone(); let api_handler_v3 = OwnerAPIHandlerV3::new(owner_api, keychain_mask.clone(), running_foreign); router .add_route("/v3/owner", Arc::new(api_handler_v3)) .map_err(|_| Error::GenericError("Router failed to add route".to_string()))?; // If so configured, add the foreign API to the same port if running_foreign { warn!("Starting HTTP Foreign API on Owner server at {}.", addr); let foreign_api_handler_v2 = ForeignAPIHandlerV2::new( wallet, owner_api.shared_config_path(), keychain_mask, test_mode, ); router .add_route("/v2/foreign", Arc::new(foreign_api_handler_v2)) .map_err(|_| Error::GenericError("Router failed to add route".to_string()))?; } let api_chan: (mpsc::Sender<()>, mpsc::Receiver<()>) = mpsc::channel::<()>(1); let mut apis = ApiServer::new(); warn!("Starting HTTP Owner API server at {}.", addr); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); let api_thread = apis .start(socket_addr, router, tls_config, api_chan) .map_err(|_| Error::GenericError("API thread failed to start".to_string()))?; warn!("HTTP Owner listener started."); api_thread .join() .map_err(|e| Error::GenericError(format!("API thread panicked :{:?}", e))) } /// Listener version, providing same API but listening for requests on a /// port and wrapping the calls pub fn foreign_listener( wallet: Arc + 'static>>>, config_path: PathBuf, bridge: Option, use_tor: Option, keychain_mask: Arc>>, addr: &str, tls_config: Option, test_mode: bool, ) -> Result<(), Error> where L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: Keychain + 'static, { loop { let (restart_tx, restart_rx) = std::sync::mpsc::channel::<()>(); register_config_listener(&config_path, "foreign_listener", restart_tx)?; let tor_config = listener_tor_config(&config_path, bridge.clone(), use_tor)?; // Check if wallet has been opened first let (sec_key, tor_dir, onion_address) = { let mask = keychain_mask.lock(); let mut w_lock = wallet.lock(); let lc = w_lock.lc_provider()?; let w_inst = lc.wallet_inst()?; let k = w_inst.keychain((&mask).as_ref())?; let parent_key_id = w_inst.parent_key_id(); let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0) .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; let tor_dir = format!("{}/tor/listener", lc.get_top_level_directory()?); let onion_address = OnionV3Address::from_private(&sec_key.0) .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; (sec_key, tor_dir, onion_address) }; let api_handler_v2 = ForeignAPIHandlerV2::new( wallet.clone(), config_path.clone().into(), keychain_mask.clone(), test_mode, ); let mut router = Router::new(); router .add_route("/v2/foreign", Arc::new(api_handler_v2)) .map_err(|_| Error::GenericError("Router failed to add route".to_string()))?; let api_chan: (mpsc::Sender<()>, mpsc::Receiver<()>) = mpsc::channel::<()>(1); let stop_tx = api_chan.0.clone(); let mut apis = ApiServer::new(); warn!("Starting HTTP Foreign listener API server at {}.", addr); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); let api_thread = apis .start(socket_addr, router, tls_config.clone(), api_chan) .map_err(|_| Error::GenericError("API thread failed to start".to_string()))?; warn!("HTTP Foreign listener started."); // Need to keep external process in scope while the listener is running. let _tor_service = if tor_config.use_tor_listener { let use_integrated = tor_config.use_integrated.unwrap_or(false); let res: Result, Error> = if use_integrated { match start_tor_service(sec_key, addr, &tor_config.clone()) { Ok(_) => Ok(None), Err(e) => { error!("Error starting integrated Tor service: {}", e); Err(e) } } } else { match init_tor_listener(sec_key, tor_dir, addr, tor_config.clone()) { Ok(p) => Ok(Some(p)), Err(e) => { error!("Error starting external Tor listener: {}", e); Err(e) } } }; match res { Ok(service) => { warn!( "Starting Tor Hidden Service for API listener at address {}, binding to {}", onion_address, addr ); let sp_address = SlatepackAddress::try_from(onion_address.clone())?; let qr_string = match QrCode::new(sp_address.to_string()) { Ok(qr) => qr.to_string(false, 3), Err(_) => "Failed to generate QR code!".to_string(), }; warn!("Slatepack Address is: {}\n{}", sp_address, qr_string); Ok(service) } Err(e) => { warn!("Unable to start TOR listener"); error!("Tor Error: {}", e); warn!("Listener is available on {}", addr); Err(e) } } } else { warn!("Listener is available on {}", addr); Ok(None) }; // Start thread to check if restart is needed. let restart_needed = Arc::new(AtomicBool::new(false)); let t_restart_needed = restart_needed.clone(); thread::spawn(move || { if restart_rx.recv().is_ok() { t_restart_needed.store(true, Ordering::SeqCst); let _ = stop_tx.try_send(()); } }); let res = api_thread .join() .map_err(|e| Error::GenericError(format!("API thread panicked :{:?}", e))); if tor_config.use_tor_listener && tor_config.use_integrated.unwrap_or(false) { stop_tor_service(onion_address.to_string())?; } if restart_needed.load(Ordering::Relaxed) { continue; } remove_global_config_listener(&config_path, "foreign_listener"); return res; } } fn register_config_listener( config_path: &PathBuf, listener_id: &str, tx: std::sync::mpsc::Sender<()>, ) -> Result<(), Error> { get_global_config(config_path)?; add_global_config_listener(config_path, listener_id, tx); Ok(()) } fn listener_tor_config( config_path: &Path, bridge: Option, use_tor: Option, ) -> Result { let mut tor_config = get_global_config(config_path)?.tor_config(); if let Some(bridge) = bridge { tor_config.bridge.bridge_line = Some(bridge); } if let Some(use_tor) = use_tor { tor_config.use_tor_listener = use_tor; } Ok(tor_config) } /// V3 API Handler/Wrapper for owner functions, which include a secure /// mode + lifecycle functions pub struct OwnerAPIHandlerV3 where L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: Keychain + 'static, { /// Wallet instance pub wallet: Arc + 'static>>>, /// Handle to Owner API owner_api: Arc>, /// ECDH shared key pub shared_key: Arc>>, /// Keychain mask (to change if also running the foreign API) pub keychain_mask: Arc>>, /// Whether we're running the foreign API on the same port, and therefore /// have to store the mask in-process pub running_foreign: bool, } pub struct OwnerV3Helpers; impl OwnerV3Helpers { /// Checks whether a request is to init the secure API pub fn is_init_secure_api(val: &serde_json::Value) -> bool { matches!(val["method"].as_str(), Some("init_secure_api")) } /// Checks whether a request is to open the wallet pub fn is_open_wallet(val: &serde_json::Value) -> bool { matches!(val["method"].as_str(), Some("open_wallet")) } /// Checks whether a request is an encrypted request pub fn is_encrypted_request(val: &serde_json::Value) -> bool { matches!(val["method"].as_str(), Some("encrypted_request_v3")) } /// whether encryption is enabled pub fn encryption_enabled(key: Arc>>) -> bool { let share_key_ref = key.lock(); share_key_ref.is_some() } /// If incoming is an encrypted request, check there is a shared key, /// Otherwise return an error value pub fn check_encryption_started( key: Arc>>, ) -> Result<(), serde_json::Value> { match OwnerV3Helpers::encryption_enabled(key) { true => Ok(()), false => Err(EncryptionErrorResponse::new( 1, -32001, "Encryption must be enabled. Please call 'init_secure_api` first", ) .as_json_value()), } } /// Update the statically held owner API shared key pub fn update_owner_api_shared_key( key: Arc>>, val: &serde_json::Value, new_key: Option, ) { if let Some(_) = val["result"]["Ok"].as_str() { let mut share_key_ref = key.lock(); *share_key_ref = new_key; } } /// Update the shared mask, in case of foreign API being run pub fn update_mask(mask: Arc>>, val: &serde_json::Value) { if let Some(key) = val["result"]["Ok"].as_str() { let key_bytes = match from_hex(key) { Ok(k) => k, Err(_) => return, }; let secp_inst = static_secp_instance(); let secp = secp_inst.lock(); let sk = match SecretKey::from_slice(&secp, &key_bytes) { Ok(s) => s, Err(_) => return, }; let mut shared_mask_ref = mask.lock(); *shared_mask_ref = Some(sk); } } /// Decrypt an encrypted request pub fn decrypt_request( key: Arc>>, req: &serde_json::Value, ) -> Result<(JsonId, serde_json::Value), serde_json::Value> { let share_key_ref = key.lock(); let shared_key = share_key_ref.as_ref().unwrap(); let enc_req: EncryptedRequest = serde_json::from_value(req.clone()).map_err(|e| { EncryptionErrorResponse::new( 1, -32002, &format!("Encrypted request format error: {}", e), ) .as_json_value() })?; let id = enc_req.id.clone(); let res = enc_req.decrypt(&shared_key).map_err(|e| { EncryptionErrorResponse::new(1, -32002, &format!("Decryption error: {}", e)) .as_json_value() })?; Ok((id, res)) } /// Encrypt a response pub fn encrypt_response( key: Arc>>, id: &JsonId, res: &serde_json::Value, ) -> Result { let share_key_ref = key.lock(); let shared_key = share_key_ref.as_ref().unwrap(); let enc_res = EncryptedResponse::from_json(id, res, &shared_key).map_err(|e| { EncryptionErrorResponse::new(1, -32003, &format!("EncryptionError: {}", e)) .as_json_value() })?; let res = enc_res.as_json_value().map_err(|e| { EncryptionErrorResponse::new( 1, -32002, &format!("Encrypted response format error: {}", e), ) .as_json_value() })?; Ok(res) } /// convert an internal error (if exists) as proper JSON-RPC pub fn check_error_response(val: &serde_json::Value) -> (bool, serde_json::Value) { // check for string first. This ensures that error messages // that are just strings aren't given weird formatting let err_string = if val["result"]["Err"].is_object() { let mut retval; let hashed: Result, serde_json::Error> = serde_json::from_value(val["result"]["Err"].clone()); retval = match hashed { Err(e) => { debug!("Can't cast value to Hashmap {}", e); None } Ok(h) => { let mut r = "".to_owned(); for (k, v) in h.iter() { r = format!("{}: {}", k, v); } Some(r) } }; // Otherwise, see if error message is a map that needs // to be stringified (and accept weird formatting) if retval.is_none() { let hashed: Result, serde_json::Error> = serde_json::from_value(val["result"]["Err"].clone()); retval = match hashed { Err(e) => { debug!("Can't cast value to Hashmap {}", e); None } Ok(h) => { let mut r = "".to_owned(); for (k, v) in h.iter() { r = format!("{}: {}", k, v); } Some(r) } } } retval } else if val["result"]["Err"].is_string() { let parsed = serde_json::from_value::(val["result"]["Err"].clone()); match parsed { Ok(p) => Some(p), Err(_) => None, } } else { None }; match err_string { Some(s) => ( true, serde_json::json!({ "jsonrpc": "2.0", "id": val["id"], "error": { "message": s, "code": -32099 } }), ), None => (false, val.clone()), } } } impl OwnerAPIHandlerV3 where L: WalletLCProvider<'static, C, K>, C: NodeClient + 'static, K: Keychain + 'static, { /// Create a new owner API handler for GET methods pub fn new( owner_api: &Owner, keychain_mask: Arc>>, running_foreign: bool, ) -> OwnerAPIHandlerV3 { let wallet = owner_api.wallet_inst.clone(); let config_path = owner_api.shared_config_path(); let owner_api = Owner::new(wallet.clone(), None, config_path); OwnerAPIHandlerV3 { wallet, owner_api: Arc::new(owner_api), shared_key: Arc::new(Mutex::new(None)), keychain_mask, running_foreign, } } async fn call_api( req: Request, key: Arc>>, mask: Arc>>, running_foreign: bool, api: Arc>, ) -> Result { let mut val: serde_json::Value = parse_body(req).await?; let mut is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val); let mut was_encrypted = false; let mut encrypted_req_id = JsonId::StrId(String::from("")); if !is_init_secure_api { if let Err(v) = OwnerV3Helpers::check_encryption_started(key.clone()) { return Ok(v); } let res = OwnerV3Helpers::decrypt_request(key.clone(), &val); match res { Err(e) => return Ok(e), Ok(v) => { encrypted_req_id = v.0.clone(); val = v.1; } } was_encrypted = true; } // check again, in case it was an encrypted call to init_secure_api is_init_secure_api = OwnerV3Helpers::is_init_secure_api(&val); // also need to intercept open/close wallet requests let is_open_wallet = OwnerV3Helpers::is_open_wallet(&val); match ::handle_request(&*api, val) { MaybeReply::Reply(mut r) => { let (_was_error, unencrypted_intercept) = OwnerV3Helpers::check_error_response(&r.clone()); if is_open_wallet && running_foreign { OwnerV3Helpers::update_mask(mask, &r.clone()); } if was_encrypted { let res = OwnerV3Helpers::encrypt_response( key.clone(), &encrypted_req_id, &unencrypted_intercept, ); r = match res { Ok(v) => v, Err(v) => return Ok(v), } } // intercept init_secure_api response (after encryption, // in case it was an encrypted call to 'init_api_secure') if is_init_secure_api { OwnerV3Helpers::update_owner_api_shared_key( key.clone(), &unencrypted_intercept, api.shared_key.lock().clone(), ); } Ok(r) } MaybeReply::DontReply => { // Since it's http, we need to return something. We return [] because jsonrpc // clients will parse it as an empty batch response. Ok(serde_json::json!([])) } } } async fn handle_post_request( req: Request, key: Arc>>, mask: Arc>>, running_foreign: bool, api: Arc>, ) -> Result, Error> { let res = Self::call_api(req, key, mask, running_foreign, api).await?; Ok(json_response_pretty(&res)) } } impl api::Handler for OwnerAPIHandlerV3 where L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: Keychain + 'static, { fn post(&self, req: Request) -> ResponseFuture { let key = self.shared_key.clone(); let mask = self.keychain_mask.clone(); let running_foreign = self.running_foreign; let api = self.owner_api.clone(); Box::pin(async move { match Self::handle_post_request(req, key, mask, running_foreign, api).await { Ok(r) => Ok(r), Err(e) => { error!("Request Error: {:?}", e); Ok(create_error_response(e)) } } }) } fn options(&self, _req: Request) -> ResponseFuture { Box::pin(async { Ok(create_ok_response("{}")) }) } } /// V2 API Handler/Wrapper for foreign functions pub struct ForeignAPIHandlerV2 where L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: Keychain + 'static, { /// Wallet instance. pub wallet: Arc + 'static>>>, /// Wallet configuration path. pub config_path: ConfigPath, /// Keychain mask pub keychain_mask: Arc>>, /// run in doctest mode pub test_mode: bool, } impl ForeignAPIHandlerV2 where L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: Keychain + 'static, { /// Create a new foreign API handler for GET methods pub fn new( wallet: Arc + 'static>>>, config_path: ConfigPath, keychain_mask: Arc>>, test_mode: bool, ) -> ForeignAPIHandlerV2 { ForeignAPIHandlerV2 { wallet, config_path, keychain_mask, test_mode, } } async fn call_api( req: Request, api: Foreign<'static, L, C, K>, ) -> Result { let val: serde_json::Value = parse_body(req).await?; match ::handle_request(&api, val) { MaybeReply::Reply(r) => Ok(r), MaybeReply::DontReply => { // Since it's http, we need to return something. We return [] because jsonrpc // clients will parse it as an empty batch response. Ok(serde_json::json!([])) } } } async fn handle_post_request( req: Request, mask: Option, wallet: Arc + 'static>>>, config_path: ConfigPath, test_mode: bool, ) -> Result, Error> { let api = Foreign::new(wallet, config_path, mask, Some(check_middleware), test_mode); let res = Self::call_api(req, api).await?; Ok(json_response_pretty(&res)) } } impl api::Handler for ForeignAPIHandlerV2 where L: WalletLCProvider<'static, C, K> + 'static, C: NodeClient + 'static, K: Keychain + 'static, { fn post(&self, req: Request) -> ResponseFuture { let mask = self.keychain_mask.lock().clone(); let wallet = self.wallet.clone(); let test_mode = self.test_mode; let config_path = self.config_path.clone(); Box::pin(async move { match Self::handle_post_request(req, mask, wallet, config_path, test_mode).await { Ok(v) => Ok(v), Err(e) => { error!("Request Error: {:?}", e); Ok(create_error_response(e)) } } }) } fn options(&self, _req: Request) -> ResponseFuture { Box::pin(async { Ok(create_ok_response("{}")) }) } } /// Utility to serialize a struct into JSON and produce a sensible Response /// out of it. fn json_response_pretty(s: &T) -> Response where T: Serialize, { match serde_json::to_string_pretty(s) { Ok(json) => response(StatusCode::OK, Full::from(json).boxed()), Err(_) => response(StatusCode::INTERNAL_SERVER_ERROR, Full::from("").boxed()), } } fn create_error_response(e: Error) -> Response { Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .header("access-control-allow-origin", "*") .header( "access-control-allow-headers", "Content-Type, Authorization", ) .body(Full::from(format!("{}", e)).boxed()) .unwrap() } fn create_ok_response(json: &str) -> Response { Response::builder() .status(StatusCode::OK) .header("access-control-allow-origin", "*") .header( "access-control-allow-headers", "Content-Type, Authorization", ) .header(hyper::header::CONTENT_TYPE, "application/json") .body(Full::from(json.to_string()).boxed()) .unwrap() } /// Build a new hyper Response with the status code and body provided. /// /// Whenever the status code is `StatusCode::OK` the text parameter should be /// valid JSON as the content type header will be set to `application/json' fn response>(status: StatusCode, text: T) -> Response { let mut builder = Response::builder() .status(status) .header("access-control-allow-origin", "*") .header( "access-control-allow-headers", "Content-Type, Authorization", ); if status == StatusCode::OK { builder = builder.header(hyper::header::CONTENT_TYPE, "application/json"); } builder.body(text.into()).unwrap() } async fn parse_body(req: Request) -> Result where for<'de> T: Deserialize<'de> + Send + 'static, { let body_req = req .into_body() .collect() .await .map_err(|e| Error::GenericError(format!("Failed to read request: {}", e)))?; let raw = body_req.to_bytes(); let cursor = Cursor::new(raw); serde_json::from_reader(cursor) .map_err(|e| Error::GenericError(format!("Invalid request body: {}", e))) } #[cfg(test)] mod tests { use super::*; use crate::core::global::ChainTypes; use grin_wallet_config::config::update_global_config; use grin_wallet_config::GlobalWalletConfig; use std::fs; use std::sync::mpsc::channel; use std::time::Duration; use uuid::Uuid; #[test] fn listener_reload() { let dir = std::env::temp_dir().join(format!("grin-wallet-listener-{}", Uuid::new_v4())); fs::create_dir(&dir).unwrap(); let path = dir.join("grin-wallet.toml"); let mut config = GlobalWalletConfig::for_chain(&ChainTypes::AutomatedTesting, &path); config .write_to_file(path.to_str().unwrap(), false, None, None) .unwrap(); let (restart_tx, restart_rx) = channel(); register_config_listener(&path, "test_listener", restart_tx).unwrap(); update_global_config(&path, |config| { config.members.tor.as_mut().unwrap().socks_proxy_addr = "127.0.0.1:59051".into(); Ok(()) }) .unwrap(); restart_rx.recv_timeout(Duration::from_secs(1)).unwrap(); let tor_config = listener_tor_config(&path, Some("obfs4 test-bridge".into()), Some(false)).unwrap(); assert_eq!(tor_config.socks_proxy_addr, "127.0.0.1:59051"); assert_eq!( tor_config.bridge.bridge_line.as_deref(), Some("obfs4 test-bridge") ); assert!(!tor_config.use_tor_listener); remove_global_config_listener(&path, "test_listener"); fs::remove_dir_all(dir).unwrap(); } }