Files
grin-wallet/controller/src/controller.rs
T
d2fe2b90f6 Global config instance (#769)
* config: global instance

* fix: cli tests

* fix: owner api tests

* config: save tor config

* fix: owner rpc tests

* fix: set config file path on init and owner api call

* tor: store launched services, get global config at command loop, remove unused listen args, check for empty global config to set default, do not save empty tor config at owner

* api: update config in memory after successful file write

* arti: restart proxy in loop

* fix: comment for tor config save

* config: set or init, update without error

* config: scoped tor config

* build: update lock file

* config: ability to store multiple configurations at memory from config path

* tor: restart service on config change

* fix: owner api test

* foreign: pass tor config from args, stop service, error handling

* arti: move runtime wrapper to single file

* config: read existing config from file at create_config

* config: handle error on read

* arti: single dir for cache

* foreign: register config listener before launch

* config: save temp

* config: remove unused error type

* config: make path and members not optional, return an error on config creation if it exists, pass default tor config to send if not set

* arti: non public runtime wrapper

* config: migrate comments

* fix: rpc tests

* config: return from cache if exists

* config: save disabled tor config when null provided at owner and tor section was not set

* config: update tor with args at listener

* config: pass pathbuf directly on creation

* config: unique tmp file name, setting permissions and providing config version

* fix global config handling

* fix config persistence

* fix tor service restart

* fix config synchronization

* require wallet close before changing data directory

* unify global config updates

---------

Co-authored-by: wiesche <wiesche89@googlemail.com>
2026-07-24 22:15:19 +02:00

964 lines
28 KiB
Rust

// 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<NodeVersionInfo>,
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<tor_process::TorProcess, Error> {
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<String, String> = 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<String, String> = 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<L, F, C, K>(
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K>>>>,
keychain_mask: Option<&SecretKey>,
config_path: PathBuf,
f: F,
) -> Result<(), Error>
where
L: WalletLCProvider<'static, C, K> + 'static,
F: FnOnce(&mut Owner<L, C, K>, 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<Mutex<Box<dyn WalletInst<'a, L, C, K>>>>,
config_path: PathBuf,
keychain_mask: Option<SecretKey>,
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<L, C, K>(
owner_api: &mut Owner<L, C, K>,
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
addr: &str,
api_secret: Option<String>,
tls_config: Option<TLSConfig>,
owner_api_include_foreign: Option<bool>,
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<L, C, K>(
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
config_path: PathBuf,
bridge: Option<String>,
use_tor: Option<bool>,
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
addr: &str,
tls_config: Option<TLSConfig>,
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<Option<TorProcess>, 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<String>,
use_tor: Option<bool>,
) -> Result<TorConfig, Error> {
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<L, C, K>
where
L: WalletLCProvider<'static, C, K> + 'static,
C: NodeClient + 'static,
K: Keychain + 'static,
{
/// Wallet instance
pub wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
/// Handle to Owner API
owner_api: Arc<Owner<L, C, K>>,
/// ECDH shared key
pub shared_key: Arc<Mutex<Option<SecretKey>>>,
/// Keychain mask (to change if also running the foreign API)
pub keychain_mask: Arc<Mutex<Option<SecretKey>>>,
/// 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<Mutex<Option<SecretKey>>>) -> 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<Mutex<Option<SecretKey>>>,
) -> 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<Mutex<Option<SecretKey>>>,
val: &serde_json::Value,
new_key: Option<SecretKey>,
) {
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<Mutex<Option<SecretKey>>>, 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<Mutex<Option<SecretKey>>>,
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<Mutex<Option<SecretKey>>>,
id: &JsonId,
res: &serde_json::Value,
) -> Result<serde_json::Value, serde_json::Value> {
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<HashMap<String, String>, serde_json::Error> =
serde_json::from_value(val["result"]["Err"].clone());
retval = match hashed {
Err(e) => {
debug!("Can't cast value to Hashmap<String> {}", 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<HashMap<String, serde_json::Value>, serde_json::Error> =
serde_json::from_value(val["result"]["Err"].clone());
retval = match hashed {
Err(e) => {
debug!("Can't cast value to Hashmap<Value> {}", 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::<String>(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<L, C, K> OwnerAPIHandlerV3<L, C, K>
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<L, C, K>,
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
running_foreign: bool,
) -> OwnerAPIHandlerV3<L, C, K> {
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<Incoming>,
key: Arc<Mutex<Option<SecretKey>>>,
mask: Arc<Mutex<Option<SecretKey>>>,
running_foreign: bool,
api: Arc<Owner<L, C, K>>,
) -> Result<serde_json::Value, Error> {
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 <dyn OwnerRpc>::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<Incoming>,
key: Arc<Mutex<Option<SecretKey>>>,
mask: Arc<Mutex<Option<SecretKey>>>,
running_foreign: bool,
api: Arc<Owner<L, C, K>>,
) -> Result<Response<ApiBody>, Error> {
let res = Self::call_api(req, key, mask, running_foreign, api).await?;
Ok(json_response_pretty(&res))
}
}
impl<L, C, K> api::Handler for OwnerAPIHandlerV3<L, C, K>
where
L: WalletLCProvider<'static, C, K> + 'static,
C: NodeClient + 'static,
K: Keychain + 'static,
{
fn post(&self, req: Request<Incoming>) -> 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<Incoming>) -> ResponseFuture {
Box::pin(async { Ok(create_ok_response("{}")) })
}
}
/// V2 API Handler/Wrapper for foreign functions
pub struct ForeignAPIHandlerV2<L, C, K>
where
L: WalletLCProvider<'static, C, K> + 'static,
C: NodeClient + 'static,
K: Keychain + 'static,
{
/// Wallet instance.
pub wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
/// Wallet configuration path.
pub config_path: ConfigPath,
/// Keychain mask
pub keychain_mask: Arc<Mutex<Option<SecretKey>>>,
/// run in doctest mode
pub test_mode: bool,
}
impl<L, C, K> ForeignAPIHandlerV2<L, C, K>
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<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
config_path: ConfigPath,
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
test_mode: bool,
) -> ForeignAPIHandlerV2<L, C, K> {
ForeignAPIHandlerV2 {
wallet,
config_path,
keychain_mask,
test_mode,
}
}
async fn call_api(
req: Request<Incoming>,
api: Foreign<'static, L, C, K>,
) -> Result<serde_json::Value, Error> {
let val: serde_json::Value = parse_body(req).await?;
match <dyn ForeignRpc>::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<Incoming>,
mask: Option<SecretKey>,
wallet: Arc<Mutex<Box<dyn WalletInst<'static, L, C, K> + 'static>>>,
config_path: ConfigPath,
test_mode: bool,
) -> Result<Response<ApiBody>, 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<L, C, K> api::Handler for ForeignAPIHandlerV2<L, C, K>
where
L: WalletLCProvider<'static, C, K> + 'static,
C: NodeClient + 'static,
K: Keychain + 'static,
{
fn post(&self, req: Request<Incoming>) -> 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<Incoming>) -> 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<T>(s: &T) -> Response<ApiBody>
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<ApiBody> {
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<ApiBody> {
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<T: Into<ApiBody>>(status: StatusCode, text: T) -> Response<ApiBody> {
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<T>(req: Request<Incoming>) -> Result<T, Error>
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();
}
}