Allow TLS for Wallet APIs (#1626)

* Allow TLS for Wallet APIs

This PR adds an optional support of TLS for wallet APIs. Only PKCS12 format is supported, will address .pem support in next PR and provide some documentation.
Address #1425
This commit is contained in:
hashmap 2018-10-02 09:49:36 +02:00 committed by GitHub
parent d7fbeb2c62
commit 4a6cae0fe6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 96 additions and 31 deletions

View file

@ -797,6 +797,7 @@ pub fn start_rest_apis(
tx_pool: Weak<RwLock<pool::TransactionPool>>, tx_pool: Weak<RwLock<pool::TransactionPool>>,
peers: Weak<p2p::Peers>, peers: Weak<p2p::Peers>,
api_secret: Option<String>, api_secret: Option<String>,
tls_config: Option<TLSConfig>,
) -> bool { ) -> bool {
let mut apis = ApiServer::new(); let mut apis = ApiServer::new();
let mut router = build_router(chain, tx_pool, peers).expect("unable to build API router"); let mut router = build_router(chain, tx_pool, peers).expect("unable to build API router");
@ -810,7 +811,7 @@ pub fn start_rest_apis(
info!(LOGGER, "Starting HTTP API server at {}.", addr); info!(LOGGER, "Starting HTTP API server at {}.", addr);
let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address");
apis.start(socket_addr, router).is_ok() apis.start(socket_addr, router, tls_config).is_ok()
} }
pub fn build_router( pub fn build_router(

View file

@ -18,7 +18,7 @@
//! To use it, just have your service(s) implement the ApiEndpoint trait and //! To use it, just have your service(s) implement the ApiEndpoint trait and
//! register them on a ApiServer. //! register them on a ApiServer.
use failure::{Backtrace, Context, Fail}; use failure::{Backtrace, Context, Fail, ResultExt};
use futures::sync::oneshot; use futures::sync::oneshot;
use futures::Stream; use futures::Stream;
use hyper::rt::Future; use hyper::rt::Future;
@ -27,6 +27,8 @@ use hyper::{rt, Body, Request, Server};
use native_tls::{Identity, TlsAcceptor}; use native_tls::{Identity, TlsAcceptor};
use router::{Handler, HandlerObj, ResponseFuture, Router}; use router::{Handler, HandlerObj, ResponseFuture, Router};
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use std::fs::File;
use std::io::Read;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::{io, thread}; use std::{io, thread};
use tokio::net::TcpListener; use tokio::net::TcpListener;
@ -95,6 +97,22 @@ pub struct TLSConfig {
pub pass: String, pub pass: String,
} }
impl TLSConfig {
pub fn new(pass: String, file: String) -> Result<TLSConfig, Error> {
let mut f = File::open(&file).context(ErrorKind::Internal(format!(
"can't open TLS certifiacte file {}",
file
)))?;
let mut pkcs_bytes = Vec::new();
f.read_to_end(&mut pkcs_bytes)
.context(ErrorKind::Internal(format!(
"can't read TLS certifiacte file {}",
file
)))?;
Ok(TLSConfig { pkcs_bytes, pass })
}
}
/// HTTP server allowing the registration of ApiEndpoint implementations. /// HTTP server allowing the registration of ApiEndpoint implementations.
pub struct ApiServer { pub struct ApiServer {
shutdown_sender: Option<oneshot::Sender<()>>, shutdown_sender: Option<oneshot::Sender<()>>,
@ -109,11 +127,25 @@ impl ApiServer {
} }
} }
/// Starts the ApiServer at the provided address. /// Starts ApiServer at the provided address.
/// TODO support stop operation
pub fn start( pub fn start(
&mut self, &mut self,
addr: SocketAddr, addr: SocketAddr,
router: Router, router: Router,
conf: Option<TLSConfig>,
) -> Result<thread::JoinHandle<()>, Error> {
match conf {
Some(conf) => self.start_tls(addr, router, conf),
None => self.start_no_tls(addr, router),
}
}
/// Starts the ApiServer at the provided address.
fn start_no_tls(
&mut self,
addr: SocketAddr,
router: Router,
) -> Result<thread::JoinHandle<()>, Error> { ) -> Result<thread::JoinHandle<()>, Error> {
if self.shutdown_sender.is_some() { if self.shutdown_sender.is_some() {
return Err(ErrorKind::Internal( return Err(ErrorKind::Internal(
@ -137,7 +169,7 @@ impl ApiServer {
/// Starts the TLS ApiServer at the provided address. /// Starts the TLS ApiServer at the provided address.
/// TODO support stop operation /// TODO support stop operation
pub fn start_tls( fn start_tls(
&mut self, &mut self,
addr: SocketAddr, addr: SocketAddr,
router: Router, router: Router,

View file

@ -69,7 +69,7 @@ fn test_start_api() {
router.add_middleware(counter.clone()); router.add_middleware(counter.clone());
let server_addr = "127.0.0.1:14434"; let server_addr = "127.0.0.1:14434";
let addr: SocketAddr = server_addr.parse().expect("unable to parse server address"); let addr: SocketAddr = server_addr.parse().expect("unable to parse server address");
assert!(server.start(addr, router).is_ok()); assert!(server.start(addr, router, None).is_ok());
let url = format!("http://{}/v1/", server_addr); let url = format!("http://{}/v1/", server_addr);
let index = api::client::get::<Vec<String>>(url.as_str(), None).unwrap(); let index = api::client::get::<Vec<String>>(url.as_str(), None).unwrap();
assert_eq!(index.len(), 2); assert_eq!(index.len(), 2);
@ -94,7 +94,7 @@ fn test_start_api_tls() {
let router = build_router(); let router = build_router();
let server_addr = "127.0.0.1:14444"; let server_addr = "127.0.0.1:14444";
let addr: SocketAddr = server_addr.parse().expect("unable to parse server address"); let addr: SocketAddr = server_addr.parse().expect("unable to parse server address");
assert!(server.start_tls(addr, router, tls_conf).is_ok()); assert!(server.start(addr, router, Some(tls_conf)).is_ok());
let url = format!("https://{}/v1/", server_addr); let url = format!("https://{}/v1/", server_addr);
let index = api::client::get::<Vec<String>>(url.as_str(), None).unwrap(); let index = api::client::get::<Vec<String>>(url.as_str(), None).unwrap();
assert_eq!(index.len(), 2); assert_eq!(index.len(), 2);

View file

@ -310,6 +310,14 @@ fn comments() -> HashMap<String, String> {
"api_listen_port".to_string(), "api_listen_port".to_string(),
" "
#port for wallet listener #port for wallet listener
#path of TLS certificate file (PKCS#12 format is supported)
#self-signed certificates are not supported, use https://github.com/FiloSottile/mkcert
#to test on localhost
#tls_certificate_file = \"\"
#password of TLS certificate file (PKCS#12 format is supported)
#tls_certificate_pass = \"\"
".to_string(), ".to_string(),
); );

View file

@ -263,6 +263,7 @@ impl Server {
Arc::downgrade(&tx_pool), Arc::downgrade(&tx_pool),
Arc::downgrade(&p2p_server.peers), Arc::downgrade(&p2p_server.peers),
api_secret, api_secret,
None,
); );
info!( info!(

View file

@ -280,6 +280,7 @@ impl LocalServerContainer {
wallet::controller::foreign_listener( wallet::controller::foreign_listener(
Box::new(wallet), Box::new(wallet),
&self.wallet_config.api_listen_addr(), &self.wallet_config.api_listen_addr(),
None,
).unwrap_or_else(|e| { ).unwrap_or_else(|e| {
panic!( panic!(
"Error creating wallet listener: {:?} Config: {:?}", "Error creating wallet listener: {:?} Config: {:?}",

View file

@ -426,7 +426,7 @@ fn replicate_tx_fluff_failure() {
let client1 = HTTPWalletClient::new("http://127.0.0.1:23003", None); let client1 = HTTPWalletClient::new("http://127.0.0.1:23003", None);
let wallet1 = create_wallet("target/tmp/tx_fluff/wallet1", client1.clone()); let wallet1 = create_wallet("target/tmp/tx_fluff/wallet1", client1.clone());
let wallet1_handle = thread::spawn(move || { let wallet1_handle = thread::spawn(move || {
controller::foreign_listener(wallet1, "127.0.0.1:33000") controller::foreign_listener(wallet1, "127.0.0.1:33000", None)
.unwrap_or_else(|e| panic!("Error creating wallet1 listener: {:?}", e,)); .unwrap_or_else(|e| panic!("Error creating wallet1 listener: {:?}", e,));
}); });
@ -434,7 +434,7 @@ fn replicate_tx_fluff_failure() {
let client2 = HTTPWalletClient::new("http://127.0.0.1:23001", None); let client2 = HTTPWalletClient::new("http://127.0.0.1:23001", None);
let wallet2 = create_wallet("target/tmp/tx_fluff/wallet2", client2.clone()); let wallet2 = create_wallet("target/tmp/tx_fluff/wallet2", client2.clone());
let wallet2_handle = thread::spawn(move || { let wallet2_handle = thread::spawn(move || {
controller::foreign_listener(wallet2, "127.0.0.1:33001") controller::foreign_listener(wallet2, "127.0.0.1:33001", None)
.unwrap_or_else(|e| panic!("Error creating wallet2 listener: {:?}", e,)); .unwrap_or_else(|e| panic!("Error creating wallet2 listener: {:?}", e,));
}); });

View file

@ -24,6 +24,7 @@ use std::time::Duration;
use clap::ArgMatches; use clap::ArgMatches;
use api::TLSConfig;
use config::GlobalWalletConfig; use config::GlobalWalletConfig;
use core::{core, global}; use core::{core, global};
use grin_wallet::{self, controller, display, libwallet}; use grin_wallet::{self, controller, display, libwallet};
@ -134,12 +135,24 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
let wallet = instantiate_wallet(wallet_config.clone(), passphrase, node_api_secret.clone()); let wallet = instantiate_wallet(wallet_config.clone(), passphrase, node_api_secret.clone());
let api_secret = get_first_line(wallet_config.api_secret_path.clone()); let api_secret = get_first_line(wallet_config.api_secret_path.clone());
let tls_conf = match wallet_config.tls_certificate_file.clone() {
None => None,
Some(file) => Some(
TLSConfig::new(
wallet_config
.tls_certificate_pass
.clone()
.expect("tls_certificate_pass must be set"),
file,
).expect("failed to configure TLS"),
),
};
match wallet_args.subcommand() { match wallet_args.subcommand() {
("listen", Some(listen_args)) => { ("listen", Some(listen_args)) => {
if let Some(port) = listen_args.value_of("port") { if let Some(port) = listen_args.value_of("port") {
wallet_config.api_listen_port = port.parse().unwrap(); wallet_config.api_listen_port = port.parse().unwrap();
} }
controller::foreign_listener(wallet, &wallet_config.api_listen_addr()) controller::foreign_listener(wallet, &wallet_config.api_listen_addr(), tls_conf)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
panic!( panic!(
"Error creating wallet listener: {:?} Config: {:?}", "Error creating wallet listener: {:?} Config: {:?}",
@ -148,26 +161,24 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
}); });
} }
("owner_api", Some(_api_args)) => { ("owner_api", Some(_api_args)) => {
controller::owner_listener(wallet, "127.0.0.1:13420", api_secret).unwrap_or_else( controller::owner_listener(wallet, "127.0.0.1:13420", api_secret, tls_conf)
|e| { .unwrap_or_else(|e| {
panic!( panic!(
"Error creating wallet api listener: {:?} Config: {:?}", "Error creating wallet api listener: {:?} Config: {:?}",
e, wallet_config e, wallet_config
) )
}, });
);
} }
("web", Some(_api_args)) => { ("web", Some(_api_args)) => {
// start owner listener and run static file server // start owner listener and run static file server
start_webwallet_server(); start_webwallet_server();
controller::owner_listener(wallet, "127.0.0.1:13420", api_secret).unwrap_or_else( controller::owner_listener(wallet, "127.0.0.1:13420", api_secret, tls_conf)
|e| { .unwrap_or_else(|e| {
panic!( panic!(
"Error creating wallet api listener: {:?} Config: {:?}", "Error creating wallet api listener: {:?} Config: {:?}",
e, wallet_config e, wallet_config
) )
}, });
);
} }
_ => {} _ => {}
}; };
@ -209,7 +220,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
let fluff = send_args.is_present("fluff"); let fluff = send_args.is_present("fluff");
let max_outputs = 500; let max_outputs = 500;
if method == "http" { if method == "http" {
if dest.starts_with("http://") { if dest.starts_with("http://") || dest.starts_with("https://") {
let result = api.issue_send_tx( let result = api.issue_send_tx(
amount, amount,
minimum_confirmations, minimum_confirmations,
@ -258,7 +269,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
} else { } else {
error!( error!(
LOGGER, LOGGER,
"HTTP Destination should start with http://: {}", dest "HTTP Destination should start with http://: or https://: {}", dest
); );
panic!(); panic!();
} }

View file

@ -15,7 +15,7 @@
//! Controller for wallet.. instantiates and handles listeners (or single-run //! Controller for wallet.. instantiates and handles listeners (or single-run
//! invocations) as needed. //! invocations) as needed.
//! Still experimental //! Still experimental
use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router}; use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig};
use core::core::Transaction; use core::core::Transaction;
use failure::ResultExt; use failure::ResultExt;
use futures::future::{err, ok}; use futures::future::{err, ok};
@ -70,6 +70,7 @@ pub fn owner_listener<T: ?Sized, C, K>(
wallet: Box<T>, wallet: Box<T>,
addr: &str, addr: &str,
api_secret: Option<String>, api_secret: Option<String>,
tls_config: Option<TLSConfig>,
) -> Result<(), Error> ) -> Result<(), Error>
where where
T: WalletBackend<C, K> + Send + Sync + 'static, T: WalletBackend<C, K> + Send + Sync + 'static,
@ -95,11 +96,11 @@ where
let mut apis = ApiServer::new(); let mut apis = ApiServer::new();
info!(LOGGER, "Starting HTTP Owner API server at {}.", addr); info!(LOGGER, "Starting HTTP Owner API server at {}.", addr);
let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address");
let api_thread = apis let api_thread =
.start(socket_addr, router) apis.start(socket_addr, router, tls_config)
.context(ErrorKind::GenericError( .context(ErrorKind::GenericError(
"API thread failed to start".to_string(), "API thread failed to start".to_string(),
))?; ))?;
api_thread api_thread
.join() .join()
.map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into()) .map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into())
@ -107,7 +108,11 @@ where
/// Listener version, providing same API but listening for requests on a /// Listener version, providing same API but listening for requests on a
/// port and wrapping the calls /// port and wrapping the calls
pub fn foreign_listener<T: ?Sized, C, K>(wallet: Box<T>, addr: &str) -> Result<(), Error> pub fn foreign_listener<T: ?Sized, C, K>(
wallet: Box<T>,
addr: &str,
tls_config: Option<TLSConfig>,
) -> Result<(), Error>
where where
T: WalletBackend<C, K> + Send + Sync + 'static, T: WalletBackend<C, K> + Send + Sync + 'static,
C: WalletClient + 'static, C: WalletClient + 'static,
@ -123,11 +128,11 @@ where
let mut apis = ApiServer::new(); let mut apis = ApiServer::new();
info!(LOGGER, "Starting HTTP Foreign API server at {}.", addr); info!(LOGGER, "Starting HTTP Foreign API server at {}.", addr);
let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address");
let api_thread = apis let api_thread =
.start(socket_addr, router) apis.start(socket_addr, router, tls_config)
.context(ErrorKind::GenericError( .context(ErrorKind::GenericError(
"API thread failed to start".to_string(), "API thread failed to start".to_string(),
))?; ))?;
api_thread api_thread
.join() .join()

View file

@ -48,6 +48,10 @@ pub struct WalletConfig {
pub check_node_api_http_addr: String, pub check_node_api_http_addr: String,
// The directory in which wallet files are stored // The directory in which wallet files are stored
pub data_file_dir: String, pub data_file_dir: String,
/// TLS ceritificate file
pub tls_certificate_file: Option<String>,
/// TLS ceritificate password
pub tls_certificate_pass: Option<String>,
} }
impl Default for WalletConfig { impl Default for WalletConfig {
@ -60,6 +64,8 @@ impl Default for WalletConfig {
node_api_secret_path: Some(".api_secret".to_string()), node_api_secret_path: Some(".api_secret".to_string()),
check_node_api_http_addr: "http://127.0.0.1:13413".to_string(), check_node_api_http_addr: "http://127.0.0.1:13413".to_string(),
data_file_dir: ".".to_string(), data_file_dir: ".".to_string(),
tls_certificate_file: None,
tls_certificate_pass: None,
} }
} }
} }