From 62fd8f21249ae7e094cf436e0da4c8020922e12a Mon Sep 17 00:00:00 2001 From: Quentin Le Sceller Date: Wed, 26 Sep 2018 16:38:44 -0400 Subject: [PATCH] Implement Basic Auth for API and Owner API (#1566) * Add api_secret * Add to base64 method * Add basic auth in API * Add Basic Auth to owner API * Add flag to enable disable basic auth * Add .api_secret file --- Cargo.lock | 2 + api/src/auth.rs | 72 ++++++++++++ api/src/client.rs | 59 ++++++---- api/src/handlers.rs | 14 ++- api/src/lib.rs | 2 + api/src/rest.rs | 12 +- api/src/router.rs | 17 ++- api/src/types.rs | 7 +- api/tests/rest.rs | 4 +- config/Cargo.toml | 1 + config/src/comments.rs | 172 ++++++++++++----------------- config/src/config.rs | 52 +++++++++ config/src/lib.rs | 5 +- servers/src/common/types.rs | 14 ++- servers/src/grin/server.rs | 19 ++-- servers/tests/api.rs | 36 +++--- servers/tests/framework/mod.rs | 2 + servers/tests/simulnet.rs | 12 +- src/bin/cmd/client.rs | 39 ++++--- src/bin/cmd/wallet.rs | 30 +++-- util/Cargo.toml | 1 + util/src/file.rs | 17 ++- util/src/lib.rs | 6 + util/src/zip.rs | 5 +- util/tests/file.rs | 8 +- util/tests/zip.rs | 6 +- wallet/src/client.rs | 19 ++-- wallet/src/libwallet/controller.rs | 19 +++- wallet/src/types.rs | 3 + 29 files changed, 427 insertions(+), 228 deletions(-) create mode 100644 api/src/auth.rs diff --git a/Cargo.lock b/Cargo.lock index aaf08d899..0d3734847 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,6 +718,7 @@ dependencies = [ "grin_util 0.3.0", "grin_wallet 0.3.0", "pretty_assertions 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.79 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.79 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -862,6 +863,7 @@ name = "grin_util" version = "0.3.0" dependencies = [ "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/api/src/auth.rs b/api/src/auth.rs new file mode 100644 index 000000000..3531ddd9a --- /dev/null +++ b/api/src/auth.rs @@ -0,0 +1,72 @@ +// Copyright 2018 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. + +use futures::future::ok; +use hyper::header::{HeaderValue, AUTHORIZATION, WWW_AUTHENTICATE}; +use hyper::{Body, Request, Response, StatusCode}; +use router::{Handler, HandlerObj, ResponseFuture}; + +// Basic Authentication Middleware +pub struct BasicAuthMiddleware { + api_basic_auth: String, + basic_realm: String, +} + +impl BasicAuthMiddleware { + pub fn new(api_basic_auth: String, basic_realm: String) -> BasicAuthMiddleware { + BasicAuthMiddleware { + api_basic_auth, + basic_realm, + } + } +} + +impl Handler for BasicAuthMiddleware { + fn call( + &self, + req: Request, + mut handlers: Box>, + ) -> ResponseFuture { + if req.headers().contains_key(AUTHORIZATION) { + if req.headers()[AUTHORIZATION] == self.api_basic_auth { + handlers.next().unwrap().call(req, handlers) + } else { + // Forbidden 403 + forbidden_response() + } + } else { + // Unauthorized 401 + unauthorized_response(&self.basic_realm) + } + } +} + +fn unauthorized_response(basic_realm: &str) -> ResponseFuture { + let response = Response::builder() + .status(StatusCode::UNAUTHORIZED) + .header( + WWW_AUTHENTICATE, + HeaderValue::from_str(basic_realm).unwrap(), + ).body(Body::empty()) + .unwrap(); + Box::new(ok(response)) +} + +fn forbidden_response() -> ResponseFuture { + let response = Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::empty()) + .unwrap(); + Box::new(ok(response)) +} diff --git a/api/src/client.rs b/api/src/client.rs index adbb3c6f6..42d0a34af 100644 --- a/api/src/client.rs +++ b/api/src/client.rs @@ -16,7 +16,7 @@ use failure::{Fail, ResultExt}; use http::uri::{InvalidUri, Uri}; -use hyper::header::{ACCEPT, USER_AGENT}; +use hyper::header::{ACCEPT, AUTHORIZATION, USER_AGENT}; use hyper::rt::{Future, Stream}; use hyper::{Body, Client, Request}; use hyper_tls; @@ -27,27 +27,28 @@ use futures::future::{err, ok, Either}; use tokio::runtime::Runtime; use rest::{Error, ErrorKind}; +use util::to_base64; pub type ClientResponseFuture = Box + Send>; /// Helper function to easily issue a HTTP GET request against a given URL that /// returns a JSON object. Handles request building, JSON deserialization and /// response code checking. -pub fn get<'a, T>(url: &'a str) -> Result +pub fn get<'a, T>(url: &'a str, api_secret: Option) -> Result where for<'de> T: Deserialize<'de>, { - handle_request(build_request(url, "GET", None)?) + handle_request(build_request(url, "GET", api_secret, None)?) } /// Helper function to easily issue an async HTTP GET request against a given /// URL that returns a future. Handles request building, JSON deserialization /// and response code checking. -pub fn get_async<'a, T>(url: &'a str) -> ClientResponseFuture +pub fn get_async<'a, T>(url: &'a str, api_secret: Option) -> ClientResponseFuture where for<'de> T: Deserialize<'de> + Send + 'static, { - match build_request(url, "GET", None) { + match build_request(url, "GET", api_secret, None) { Ok(req) => Box::new(handle_request_async(req)), Err(e) => Box::new(err(e)), } @@ -57,12 +58,12 @@ where /// object as body on a given URL that returns a JSON object. Handles request /// building, JSON serialization and deserialization, and response code /// checking. -pub fn post(url: &str, input: &IN) -> Result +pub fn post(url: &str, api_secret: Option, input: &IN) -> Result where IN: Serialize, for<'de> OUT: Deserialize<'de>, { - let req = create_post_request(url, input)?; + let req = create_post_request(url, api_secret, input)?; handle_request(req) } @@ -70,13 +71,17 @@ where /// provided JSON object as body on a given URL that returns a future. Handles /// request building, JSON serialization and deserialization, and response code /// checking. -pub fn post_async(url: &str, input: &IN) -> ClientResponseFuture +pub fn post_async( + url: &str, + input: &IN, + api_secret: Option, +) -> ClientResponseFuture where IN: Serialize, OUT: Send + 'static, for<'de> OUT: Deserialize<'de>, { - match create_post_request(url, input) { + match create_post_request(url, api_secret, input) { Ok(req) => Box::new(handle_request_async(req)), Err(e) => Box::new(err(e)), } @@ -86,11 +91,11 @@ where /// object as body on a given URL that returns nothing. Handles request /// building, JSON serialization, and response code /// checking. -pub fn post_no_ret(url: &str, input: &IN) -> Result<(), Error> +pub fn post_no_ret(url: &str, api_secret: Option, input: &IN) -> Result<(), Error> where IN: Serialize, { - let req = create_post_request(url, input)?; + let req = create_post_request(url, api_secret, input)?; send_request(req)?; Ok(()) } @@ -99,11 +104,15 @@ where /// provided JSON object as body on a given URL that returns a future. Handles /// request building, JSON serialization and deserialization, and response code /// checking. -pub fn post_no_ret_async(url: &str, input: &IN) -> ClientResponseFuture<()> +pub fn post_no_ret_async( + url: &str, + api_secret: Option, + input: &IN, +) -> ClientResponseFuture<()> where IN: Serialize, { - match create_post_request(url, input) { + match create_post_request(url, api_secret, input) { Ok(req) => Box::new(send_request_async(req).and_then(|_| ok(()))), Err(e) => Box::new(err(e)), } @@ -112,13 +121,21 @@ where fn build_request<'a>( url: &'a str, method: &str, + api_secret: Option, body: Option, ) -> Result, Error> { let uri = url.parse::().map_err::(|e: InvalidUri| { e.context(ErrorKind::Argument(format!("Invalid url {}", url))) .into() })?; - Request::builder() + let mut builder = Request::builder(); + if api_secret.is_some() { + let basic_auth = + "Basic ".to_string() + &to_base64(&("grin:".to_string() + &api_secret.unwrap())); + builder.header(AUTHORIZATION, basic_auth); + } + + builder .method(method) .uri(uri) .header(USER_AGENT, "grin-client") @@ -126,20 +143,23 @@ fn build_request<'a>( .body(match body { None => Body::empty(), Some(json) => json.into(), - }) - .map_err(|e| { + }).map_err(|e| { ErrorKind::RequestError(format!("Bad request {} {}: {}", method, url, e)).into() }) } -fn create_post_request(url: &str, input: &IN) -> Result, Error> +fn create_post_request( + url: &str, + api_secret: Option, + input: &IN, +) -> Result, Error> where IN: Serialize, { let json = serde_json::to_string(input).context(ErrorKind::Internal( "Could not serialize data to JSON".to_owned(), ))?; - build_request(url, "POST", Some(json)) + build_request(url, "POST", api_secret, Some(json)) } fn handle_request(req: Request) -> Result @@ -183,8 +203,7 @@ fn send_request_async(req: Request) -> Box, tx_pool: Weak>, peers: Weak, + api_secret: Option, ) -> bool { let mut apis = ApiServer::new(); - - let 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"); + if api_secret.is_some() { + let api_basic_auth = + "Basic ".to_string() + &util::to_base64(&("grin:".to_string() + &api_secret.unwrap())); + let basic_realm = "Basic realm=GrinAPI".to_string(); + let basic_auth_middleware = Arc::new(BasicAuthMiddleware::new(api_basic_auth, basic_realm)); + router.add_middleware(basic_auth_middleware); + } info!(LOGGER, "Starting HTTP API server at {}.", addr); let socket_addr: SocketAddr = addr.parse().expect("unable to parse socket address"); @@ -875,7 +883,7 @@ pub fn build_router( }; let mut router = Router::new(); - // example how we can use midlleware + router.add_route("/v1/", Arc::new(index_handler))?; router.add_route("/v1/blocks/*", Arc::new(block_handler))?; router.add_route("/v1/headers/*", Arc::new(header_handler))?; diff --git a/api/src/lib.rs b/api/src/lib.rs index 0c4a75375..41be1cf71 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -41,6 +41,7 @@ extern crate tokio; extern crate tokio_core; extern crate tokio_tls; +pub mod auth; pub mod client; mod handlers; mod rest; @@ -48,6 +49,7 @@ mod router; mod types; mod web; +pub use auth::BasicAuthMiddleware; pub use handlers::start_rest_apis; pub use rest::*; pub use router::*; diff --git a/api/src/rest.rs b/api/src/rest.rs index 1fe66b7fe..38cdd9d93 100644 --- a/api/src/rest.rs +++ b/api/src/rest.rs @@ -132,8 +132,7 @@ impl ApiServer { .map_err(|e| eprintln!("HTTP API server error: {}", e)); rt::run(server); - }) - .map_err(|_| ErrorKind::Internal("failed to spawn API thread".to_string()).into()) + }).map_err(|_| ErrorKind::Internal("failed to spawn API thread".to_string()).into()) } /// Starts the TLS ApiServer at the provided address. @@ -165,15 +164,13 @@ impl ApiServer { .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) }), router, - ) - .then(|res| match res { + ).then(|res| match res { Ok(conn) => Ok(Some(conn)), Err(e) => { eprintln!("Error: {}", e); Ok(None) } - }) - .for_each(|conn_opt| { + }).for_each(|conn_opt| { if let Some(conn) = conn_opt { rt::spawn( conn.and_then(|c| c.map_err(|e| panic!("Hyper error {}", e))) @@ -184,8 +181,7 @@ impl ApiServer { }); rt::run(server); - }) - .map_err(|_| ErrorKind::Internal("failed to spawn API thread".to_string()).into()) + }).map_err(|_| ErrorKind::Internal("failed to spawn API thread".to_string()).into()) } /// Stops the API server, it panics in case of error diff --git a/api/src/router.rs b/api/src/router.rs index edf1604ca..5f24b033b 100644 --- a/api/src/router.rs +++ b/api/src/router.rs @@ -1,3 +1,17 @@ +// Copyright 2018 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. + use futures::future; use hyper; use hyper::rt::Future; @@ -133,8 +147,7 @@ impl Router { .find(|&id| { let node_key = self.node(*id).key; node_key == key || node_key == *WILDCARD_HASH || node_key == *WILDCARD_STOP_HASH - }) - .cloned() + }).cloned() } fn add_empty_node(&mut self, parent: NodeId, key: u64) -> NodeId { diff --git a/api/src/types.rs b/api/src/types.rs index 85eda3e84..1e3818d84 100644 --- a/api/src/types.rs +++ b/api/src/types.rs @@ -280,8 +280,8 @@ impl OutputPrintable { let mut merkle_proof = None; if output .features - .contains(core::transaction::OutputFeatures::COINBASE_OUTPUT) && !spent - && block_header.is_some() + .contains(core::transaction::OutputFeatures::COINBASE_OUTPUT) + && !spent && block_header.is_some() { merkle_proof = chain.get_merkle_proof(&out_id, &block_header.unwrap()).ok() }; @@ -564,8 +564,7 @@ impl BlockPrintable { Some(&block.header), include_proof, ) - }) - .collect(); + }).collect(); let kernels = block .kernels() .iter() diff --git a/api/tests/rest.rs b/api/tests/rest.rs index 944a43279..ccc8579bb 100644 --- a/api/tests/rest.rs +++ b/api/tests/rest.rs @@ -71,7 +71,7 @@ fn test_start_api() { let addr: SocketAddr = server_addr.parse().expect("unable to parse server address"); assert!(server.start(addr, router).is_ok()); let url = format!("http://{}/v1/", server_addr); - let index = api::client::get::>(url.as_str()).unwrap(); + let index = api::client::get::>(url.as_str(), None).unwrap(); assert_eq!(index.len(), 2); assert_eq!(counter.value(), 1); assert!(server.stop()); @@ -96,7 +96,7 @@ fn test_start_api_tls() { let addr: SocketAddr = server_addr.parse().expect("unable to parse server address"); assert!(server.start_tls(addr, router, tls_conf).is_ok()); let url = format!("https://{}/v1/", server_addr); - let index = api::client::get::>(url.as_str()).unwrap(); + let index = api::client::get::>(url.as_str(), None).unwrap(); assert_eq!(index.len(), 2); assert!(!server.stop()); } diff --git a/config/Cargo.toml b/config/Cargo.toml index ed03b0f8e..bea1c05a3 100644 --- a/config/Cargo.toml +++ b/config/Cargo.toml @@ -6,6 +6,7 @@ workspace = ".." publish = false [dependencies] +rand = "0.5" serde = "1" serde_derive = "1" toml = "0.4" diff --git a/config/src/comments.rs b/config/src/comments.rs index 9abb9387b..7b1460cfe 100644 --- a/config/src/comments.rs +++ b/config/src/comments.rs @@ -36,16 +36,22 @@ fn comments() -> HashMap { ######################################### #Server connection details -" - .to_string(), +".to_string(), ); retval.insert( "api_http_addr".to_string(), " #the address on which services will listen, e.g. Transaction Pool -" - .to_string(), +".to_string(), + ); + + retval.insert( + "api_secret_path".to_string(), + " +#path of the secret token used by the API to authenticate the calls +#comment the it to disable basic auth +".to_string(), ); retval.insert( @@ -53,8 +59,7 @@ fn comments() -> HashMap { " #the directory, relative to current, in which the grin blockchain #is stored -" - .to_string(), +".to_string(), ); retval.insert( @@ -66,35 +71,31 @@ fn comments() -> HashMap { #UserTesting - For regular user testing (cuckoo 16) #Testnet1 - Testnet1 genesis block (cuckoo 16) #Testnet2 - Testnet2 genesis block (cuckoo 30) -" - .to_string(), +".to_string(), ); retval.insert( "chain_validation_mode".to_string(), " -#The chain validation mode, defines how often (if at all) we +#the chain validation mode, defines how often (if at all) we #want to run a full chain validation. Can be: #\"EveryBlock\" - run full chain validation when processing each block (except during sync) #\"Disabled\" - disable full chain validation (just run regular block validation) -" - .to_string(), +".to_string(), ); retval.insert( "archive_mode".to_string(), " #run the node in \"full archive\" mode (default is fast-sync, pruned node) -" - .to_string(), +".to_string(), ); retval.insert( "skip_sync_wait".to_string(), " #skip waiting for sync on startup, (optional param, mostly for testing) -" - .to_string(), +".to_string(), ); retval.insert( @@ -102,8 +103,7 @@ fn comments() -> HashMap { " #whether to run the ncurses TUI. Ncurses must be installed and this #will also disable logging to stdout -" - .to_string(), +".to_string(), ); retval.insert( @@ -112,8 +112,7 @@ fn comments() -> HashMap { #Whether to run a test miner. This is only for developer testing (chaintype #usertesting) at cuckoo 16, and will only mine into the default wallet port. #real mining should use the standalone grin-miner -" - .to_string(), +".to_string(), ); retval.insert( @@ -122,39 +121,34 @@ fn comments() -> HashMap { ######################################### ### DANDELION CONFIGURATION ### ######################################### -" - .to_string(), +".to_string(), ); retval.insert( "relay_secs".to_string(), " #dandelion relay time (choose new relay peer every n secs) -" - .to_string(), +".to_string(), ); retval.insert( "embargo_secs".to_string(), " #fluff and broadcast after embargo expires if tx not seen on network -" - .to_string(), +".to_string(), ); retval.insert( "patience_secs".to_string(), " #run dandelion stem/fluff processing every n secs (stem tx aggregation in this window) -" - .to_string(), +".to_string(), ); retval.insert( "stem_probability".to_string(), " #dandelion stem probability (stem 90% of the time, fluff 10% of the time) -" - .to_string(), +".to_string(), ); retval.insert( @@ -166,26 +160,23 @@ fn comments() -> HashMap { ### SERVER P2P CONFIGURATION ### ######################################### #The P2P server details (i.e. the server that communicates with other -" - .to_string(), +".to_string(), ); retval.insert( "host".to_string(), " #The interface on which to listen. -#0.0.0.0 will listen on all interfaces, alowing others to interact +#0.0.0.0 will listen on all interfaces, allowing others to interact #127.0.0.1 will listen on the local machine only -" - .to_string(), +".to_string(), ); retval.insert( "port".to_string(), " #The port on which to listen. -" - .to_string(), +".to_string(), ); retval.insert( @@ -213,17 +204,15 @@ fn comments() -> HashMap { #until we get to at least this number #peer_min_preferred_count = 8 -#How to seed this server, can be None, List or DNSSeed -" - .to_string(), +#how to seed this server, can be None, List or DNSSeed +".to_string(), ); retval.insert( "[server.p2p_config.capabilities]".to_string(), "#7 = Bit flags for FULL_NODE, this structure needs to be changed #internally to make it more configurable -" - .to_string(), +".to_string(), ); retval.insert( @@ -232,24 +221,21 @@ fn comments() -> HashMap { ######################################### ### MEMPOOL CONFIGURATION ### ######################################### -" - .to_string(), +".to_string(), ); retval.insert( "accept_fee_base".to_string(), " -#Base fee that's accepted into the pool -" - .to_string(), +#base fee that's accepted into the pool +".to_string(), ); retval.insert( "max_pool_size".to_string(), " -#Maximum number of transactions allowed in the pool -" - .to_string(), +#maximum number of transactions allowed in the pool +".to_string(), ); retval.insert( @@ -258,57 +244,50 @@ fn comments() -> HashMap { ################################################ ### STRATUM MINING SERVER CONFIGURATION ### ################################################ -" - .to_string(), +".to_string(), ); retval.insert( "enable_stratum_server".to_string(), " #whether stratum server is enabled -" - .to_string(), +".to_string(), ); retval.insert( "stratum_server_addr".to_string(), " #what port and address for the stratum server to listen on -" - .to_string(), +".to_string(), ); retval.insert( "attempt_time_per_block".to_string(), " -#The amount of time, in seconds, to attempt to mine on a particular +#the amount of time, in seconds, to attempt to mine on a particular #header before stopping and re-collecting transactions from the pool -" - .to_string(), +".to_string(), ); retval.insert( "minimum_share_difficulty".to_string(), " -#The minimum acceptable share difficulty to request from miners -" - .to_string(), +#the minimum acceptable share difficulty to request from miners +".to_string(), ); retval.insert( "wallet_listener_url".to_string(), " #the wallet receiver to which coinbase rewards will be sent -" - .to_string(), +".to_string(), ); retval.insert( "burn_reward".to_string(), " #whether to ignore the reward (mostly for testing) -" - .to_string(), +".to_string(), ); retval.insert( @@ -317,40 +296,42 @@ fn comments() -> HashMap { ######################################### ### WALLET CONFIGURATION ### ######################################### -" - .to_string(), +".to_string(), ); retval.insert( "api_listen_interface".to_string(), " -# Host IP for wallet listener, change to \"0.0.0.0\" to receive grins -" - .to_string(), +#host IP for wallet listener, change to \"0.0.0.0\" to receive grins +".to_string(), ); retval.insert( "api_listen_port".to_string(), " -# Port for wallet listener -" - .to_string(), +#port for wallet listener +".to_string(), ); + retval.insert( + "api_secret_path".to_string(), + " +#path of the secret token used by the API to authenticate the calls +#comment it to disable basic auth +".to_string(), + ); retval.insert( "check_node_api_http_addr".to_string(), " -# Where the wallet should find a running node -" - .to_string(), +#where the wallet should find a running node +".to_string(), ); retval.insert( "data_file_dir".to_string(), " -# Where to find wallet files (seed, data, etc) -" - .to_string(), +#where to find wallet files (seed, data, etc) +".to_string(), ); retval.insert( @@ -359,56 +340,49 @@ fn comments() -> HashMap { ######################################### ### LOGGING CONFIGURATION ### ######################################### -" - .to_string(), +".to_string(), ); retval.insert( "log_to_stdout".to_string(), " -# Whether to log to stdout -" - .to_string(), +#whether to log to stdout +".to_string(), ); retval.insert( "stdout_log_level".to_string(), " -# Log level for stdout: Critical, Error, Warning, Info, Debug, Trace -" - .to_string(), +#log level for stdout: Critical, Error, Warning, Info, Debug, Trace +".to_string(), ); retval.insert( "log_to_file".to_string(), " -# Whether to log to a file -" - .to_string(), +#whether to log to a file +".to_string(), ); retval.insert( "file_log_level".to_string(), " -# Log level for file: Critical, Error, Warning, Info, Debug, Trace -" - .to_string(), +#log level for file: Critical, Error, Warning, Info, Debug, Trace +".to_string(), ); retval.insert( "log_file_path".to_string(), " -# Log file path -" - .to_string(), +#log file path +".to_string(), ); retval.insert( "log_file_append".to_string(), " -# Whether to append to the log file (true), or replace it on every run (false) -" - .to_string(), +#whether to append to the log file (true), or replace it on every run (false) +".to_string(), ); retval diff --git a/config/src/config.rs b/config/src/config.rs index 9b81e4d2f..6ab4d99dd 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -15,9 +15,12 @@ //! Configuration file management use dirs; +use rand::distributions::{Alphanumeric, Distribution}; +use rand::thread_rng; use std::env; use std::fs::{self, File}; use std::io::prelude::*; +use std::io::BufReader; use std::io::Read; use std::path::PathBuf; use toml; @@ -40,6 +43,7 @@ const WALLET_LOG_FILE_NAME: &'static str = "grin-wallet.log"; const GRIN_HOME: &'static str = ".grin"; const GRIN_CHAIN_DIR: &'static str = "chain_data"; const GRIN_WALLET_DIR: &'static str = "wallet_data"; +const API_SECRET_FILE_NAME: &'static str = ".api_secret"; fn get_grin_path() -> Result { // Check if grin dir exists @@ -78,8 +82,45 @@ fn check_config_current_dir(path: &str) -> Option { None } +/// Create file with api secret +fn init_api_secret(api_secret_path: &PathBuf) -> Result<(), ConfigError> { + let mut api_secret_file = File::create(api_secret_path)?; + let api_secret: String = Alphanumeric + .sample_iter(&mut thread_rng()) + .take(20) + .collect(); + api_secret_file.write_all(api_secret.as_bytes())?; + Ok(()) +} + +/// // Check if file contains a secret and nothing else +fn check_api_secret(api_secret_path: &PathBuf) -> Result<(), ConfigError> { + let api_secret_file = File::open(api_secret_path)?; + let buf_reader = BufReader::new(api_secret_file); + let mut lines_iter = buf_reader.lines(); + let first_line = lines_iter.next(); + if first_line.is_none() || first_line.unwrap().is_err() { + fs::remove_file(api_secret_path)?; + init_api_secret(api_secret_path)?; + } + Ok(()) +} + +/// Check that the api secret file exists and is valid +pub fn check_api_secret_file() -> Result<(), ConfigError> { + let grin_path = get_grin_path()?; + let mut api_secret_path = grin_path.clone(); + api_secret_path.push(API_SECRET_FILE_NAME); + if !api_secret_path.exists() { + init_api_secret(&api_secret_path) + } else { + check_api_secret(&api_secret_path) + } +} + /// Handles setup and detection of paths for node pub fn initial_setup_server() -> Result { + check_api_secret_file()?; // Use config file if current directory if it exists, .grin home otherwise if let Some(p) = check_config_current_dir(SERVER_CONFIG_FILE_NAME) { GlobalConfig::new(p.to_str().unwrap()) @@ -98,12 +139,14 @@ pub fn initial_setup_server() -> Result { default_config.update_paths(&grin_path); default_config.write_to_file(config_path.to_str().unwrap())?; } + GlobalConfig::new(config_path.to_str().unwrap()) } } /// Handles setup and detection of paths for wallet pub fn initial_setup_wallet() -> Result { + check_api_secret_file()?; // Use config file if current directory if it exists, .grin home otherwise if let Some(p) = check_config_current_dir(WALLET_CONFIG_FILE_NAME) { GlobalWalletConfig::new(p.to_str().unwrap()) @@ -122,6 +165,7 @@ pub fn initial_setup_wallet() -> Result { default_config.update_paths(&grin_path); default_config.write_to_file(config_path.to_str().unwrap())?; } + GlobalWalletConfig::new(config_path.to_str().unwrap()) } } @@ -216,6 +260,10 @@ impl GlobalConfig { let mut chain_path = grin_home.clone(); chain_path.push(GRIN_CHAIN_DIR); self.members.as_mut().unwrap().server.db_root = chain_path.to_str().unwrap().to_owned(); + let mut secret_path = grin_home.clone(); + secret_path.push(API_SECRET_FILE_NAME); + self.members.as_mut().unwrap().server.api_secret_path = + Some(secret_path.to_str().unwrap().to_owned()); let mut log_path = grin_home.clone(); log_path.push(SERVER_LOG_FILE_NAME); self.members @@ -319,6 +367,10 @@ impl GlobalWalletConfig { wallet_path.push(GRIN_WALLET_DIR); self.members.as_mut().unwrap().wallet.data_file_dir = wallet_path.to_str().unwrap().to_owned(); + let mut secret_path = wallet_home.clone(); + secret_path.push(API_SECRET_FILE_NAME); + self.members.as_mut().unwrap().wallet.api_secret_path = + Some(secret_path.to_str().unwrap().to_owned()); let mut log_path = wallet_home.clone(); log_path.push(WALLET_LOG_FILE_NAME); self.members diff --git a/config/src/lib.rs b/config/src/lib.rs index 63e93005d..22b6a509f 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -20,9 +20,10 @@ #![deny(unused_mut)] #![warn(missing_docs)] +extern crate dirs; +extern crate rand; #[macro_use] extern crate serde_derive; -extern crate dirs; extern crate toml; extern crate grin_p2p as p2p; @@ -34,5 +35,5 @@ mod comments; pub mod config; pub mod types; -pub use config::{initial_setup_server, initial_setup_wallet}; +pub use config::{check_api_secret_file, initial_setup_server, initial_setup_wallet}; pub use types::{ConfigError, ConfigMembers, GlobalConfig, GlobalWalletConfig}; diff --git a/servers/src/common/types.rs b/servers/src/common/types.rs index 9d2a5f7c2..6c9c8eb33 100644 --- a/servers/src/common/types.rs +++ b/servers/src/common/types.rs @@ -13,7 +13,6 @@ // limitations under the License. //! Server types - use std::convert::From; use std::sync::{Arc, RwLock}; @@ -113,6 +112,9 @@ pub struct ServerConfig { /// Network address for the Rest API HTTP server. pub api_http_addr: String, + /// Location of secret for basic auth on Rest API HTTP server. + pub api_secret_path: Option, + /// Setup the server for tests, testnet or mainnet #[serde(default)] pub chain_type: ChainTypes, @@ -163,11 +165,10 @@ impl ServerConfig { // check [server.p2p_config.capabilities] with 'archive_mode' in [server] if let Some(archive) = self.archive_mode { // note: slog not available before config loaded, only print here. - if archive - != self - .p2p_config - .capabilities - .contains(p2p::Capabilities::FULL_HIST) + if archive != self + .p2p_config + .capabilities + .contains(p2p::Capabilities::FULL_HIST) { // if conflict, 'archive_mode' win self.p2p_config @@ -185,6 +186,7 @@ impl Default for ServerConfig { ServerConfig { db_root: "grin_chain".to_string(), api_http_addr: "127.0.0.1:13413".to_string(), + api_secret_path: Some(".api_secret".to_string()), p2p_config: p2p::P2PConfig::default(), dandelion_config: pool::DandelionConfig::default(), stratum_mining_config: Some(StratumServerConfig::default()), diff --git a/servers/src/grin/server.rs b/servers/src/grin/server.rs index 5a7159c08..c3a36ea80 100644 --- a/servers/src/grin/server.rs +++ b/servers/src/grin/server.rs @@ -38,6 +38,7 @@ use mining::test_miner::Miner; use p2p; use pool; use store; +use util::file::get_first_line; use util::LOGGER; /// Grin server holding internal structures. @@ -112,11 +113,10 @@ impl Server { }; // If archive mode is enabled then the flags should contains the FULL_HIST flag - if archive_mode - && !config - .p2p_config - .capabilities - .contains(p2p::Capabilities::FULL_HIST) + if archive_mode && !config + .p2p_config + .capabilities + .contains(p2p::Capabilities::FULL_HIST) { config .p2p_config @@ -256,12 +256,13 @@ impl Server { .spawn(move || p2p_inner.listen()); info!(LOGGER, "Starting rest apis at: {}", &config.api_http_addr); - + let api_secret = get_first_line(config.api_secret_path.clone()); api::start_rest_apis( config.api_http_addr.clone(), Arc::downgrade(&shared_chain), Arc::downgrade(&tx_pool), Arc::downgrade(&p2p_server.peers), + api_secret, ); info!( @@ -424,8 +425,7 @@ impl Server { time: time, duration: dur, } - }) - .collect(); + }).collect(); let block_time_sum = diff_entries.iter().fold(0, |sum, t| sum + t.duration); let block_diff_sum = diff_entries.iter().fold(0, |sum, d| sum + d.difficulty); @@ -446,8 +446,7 @@ impl Server { .map(|p| { let p = p.read().unwrap(); PeerStats::from_peer(&p) - }) - .collect(); + }).collect(); Ok(ServerStats { peer_count: self.peer_count(), head: self.head(), diff --git a/servers/tests/api.rs b/servers/tests/api.rs index 08a130a32..e09fc2218 100644 --- a/servers/tests/api.rs +++ b/servers/tests/api.rs @@ -240,13 +240,13 @@ fn test_p2p() { // Tip handler function fn get_tip(base_addr: &String, api_server_port: u16) -> Result { let url = format!("http://{}:{}/v1/chain", base_addr, api_server_port); - api::client::get::(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } // Status handler function fn get_status(base_addr: &String, api_server_port: u16) -> Result { let url = format!("http://{}:{}/v1/status", base_addr, api_server_port); - api::client::get::(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } // Block handler functions @@ -259,7 +259,7 @@ fn get_block_by_height( "http://{}:{}/v1/blocks/{}", base_addr, api_server_port, height ); - api::client::get::(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } fn get_block_by_height_compact( @@ -271,7 +271,7 @@ fn get_block_by_height_compact( "http://{}:{}/v1/blocks/{}?compact", base_addr, api_server_port, height ); - api::client::get::(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } fn get_block_by_hash( @@ -283,7 +283,7 @@ fn get_block_by_hash( "http://{}:{}/v1/blocks/{}", base_addr, api_server_port, block_hash ); - api::client::get::(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } fn get_block_by_hash_compact( @@ -295,7 +295,7 @@ fn get_block_by_hash_compact( "http://{}:{}/v1/blocks/{}?compact", base_addr, api_server_port, block_hash ); - api::client::get::(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } // Chain output handler functions @@ -310,7 +310,7 @@ fn get_outputs_by_ids1( api_server_port, ids.join(",") ); - api::client::get::>(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) } fn get_outputs_by_ids2( @@ -327,7 +327,7 @@ fn get_outputs_by_ids2( "http://{}:{}/v1/chain/outputs/byids?{}", base_addr, api_server_port, ids_string ); - api::client::get::>(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) } fn get_outputs_by_height( @@ -340,7 +340,7 @@ fn get_outputs_by_height( "http://{}:{}/v1/chain/outputs/byheight?start_height={}&end_height={}", base_addr, api_server_port, start_height, end_height ); - api::client::get::>(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) } // TxHashSet handler functions @@ -349,7 +349,7 @@ fn get_txhashset_roots(base_addr: &String, api_server_port: u16) -> Result(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } fn get_txhashset_lastoutputs( @@ -369,7 +369,7 @@ fn get_txhashset_lastoutputs( base_addr, api_server_port, n ); } - api::client::get::>(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) } fn get_txhashset_lastrangeproofs( @@ -389,7 +389,7 @@ fn get_txhashset_lastrangeproofs( base_addr, api_server_port, n ); } - api::client::get::>(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) } fn get_txhashset_lastkernels( @@ -409,7 +409,7 @@ fn get_txhashset_lastkernels( base_addr, api_server_port, n ); } - api::client::get::>(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) } // Helper function to get a vec of commitment output ids from a vec of block @@ -430,7 +430,7 @@ pub fn ban_peer(base_addr: &String, api_server_port: u16, peer_addr: &String) -> "http://{}:{}/v1/peers/{}/ban", base_addr, api_server_port, peer_addr ); - api::client::post_no_ret(url.as_str(), &"").map_err(|e| Error::API(e)) + api::client::post_no_ret(url.as_str(), None, &"").map_err(|e| Error::API(e)) } pub fn unban_peer( @@ -442,7 +442,7 @@ pub fn unban_peer( "http://{}:{}/v1/peers/{}/unban", base_addr, api_server_port, peer_addr ); - api::client::post_no_ret(url.as_str(), &"").map_err(|e| Error::API(e)) + api::client::post_no_ret(url.as_str(), None, &"").map_err(|e| Error::API(e)) } pub fn get_peer( @@ -454,7 +454,7 @@ pub fn get_peer( "http://{}:{}/v1/peers/{}", base_addr, api_server_port, peer_addr ); - api::client::get::(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } pub fn get_connected_peers( @@ -465,7 +465,7 @@ pub fn get_connected_peers( "http://{}:{}/v1/peers/connected", base_addr, api_server_port ); - api::client::get::>(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) } pub fn get_all_peers( @@ -473,7 +473,7 @@ pub fn get_all_peers( api_server_port: u16, ) -> Result, Error> { let url = format!("http://{}:{}/v1/peers/all", base_addr, api_server_port); - api::client::get::>(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) } /// Error type wrapping underlying module errors. diff --git a/servers/tests/framework/mod.rs b/servers/tests/framework/mod.rs index db24b6515..190718063 100644 --- a/servers/tests/framework/mod.rs +++ b/servers/tests/framework/mod.rs @@ -188,6 +188,7 @@ impl LocalServerContainer { let s = servers::Server::new(servers::ServerConfig { api_http_addr: api_addr, + api_secret_path: None, db_root: format!("{}/.grin", self.working_dir), p2p_config: p2p::P2PConfig { port: self.config.p2p_server_port, @@ -581,6 +582,7 @@ pub fn stop_all_servers(servers: Arc>>) { pub fn config(n: u16, test_name_dir: &str, seed_n: u16) -> servers::ServerConfig { servers::ServerConfig { api_http_addr: format!("127.0.0.1:{}", 20000 + n), + api_secret_path: None, db_root: format!("target/tmp/{}/grin-sync-{}", test_name_dir, n), p2p_config: p2p::P2PConfig { port: 10000 + n, diff --git a/servers/tests/simulnet.rs b/servers/tests/simulnet.rs index 2b0474ec5..c1e61a705 100644 --- a/servers/tests/simulnet.rs +++ b/servers/tests/simulnet.rs @@ -127,7 +127,7 @@ fn simulate_seeding() { "http://{}:{}/v1/peers/connected", &server_config.base_addr, 30020 ); - let peers_all = api::client::get::>(url.as_str()); + let peers_all = api::client::get::>(url.as_str(), None); assert!(peers_all.is_ok()); assert_eq!(peers_all.unwrap().len(), 4); @@ -279,7 +279,11 @@ fn simulate_full_sync() { thread::sleep(time::Duration::from_millis(1_000)); time_spent += 1; if time_spent >= 60 { - println!("sync fail. s2.head().height: {}, s1_header.height: {}", s2.head().height, s1_header.height); + println!( + "sync fail. s2.head().height: {}, s1_header.height: {}", + s2.head().height, + s1_header.height + ); break; } } @@ -475,8 +479,8 @@ fn replicate_tx_fluff_failure() { let mut slate = Slate::blank(1); wallet::controller::owner_single_use(wallet1.clone(), |api| { - slate = - api.issue_send_tx( + slate = api + .issue_send_tx( amount, // amount 2, // minimum confirmations "http://127.0.0.1:33001", // dest diff --git a/src/bin/cmd/client.rs b/src/bin/cmd/client.rs index 714c9521d..94c16c13b 100644 --- a/src/bin/cmd/client.rs +++ b/src/bin/cmd/client.rs @@ -22,23 +22,25 @@ use config::GlobalConfig; use p2p; use servers::ServerConfig; use term; +use util::file::get_first_line; pub fn client_command(client_args: &ArgMatches, global_config: GlobalConfig) { // just get defaults from the global config let server_config = global_config.members.unwrap().server; + let api_secret = get_first_line(server_config.api_secret_path.clone()); match client_args.subcommand() { ("status", Some(_)) => { - show_status(&server_config); + show_status(&server_config, api_secret); } ("listconnectedpeers", Some(_)) => { - list_connected_peers(&server_config); + list_connected_peers(&server_config, api_secret); } ("ban", Some(peer_args)) => { let peer = peer_args.value_of("peer").unwrap(); if let Ok(addr) = peer.parse() { - ban_peer(&server_config, &addr); + ban_peer(&server_config, &addr, api_secret); } else { panic!("Invalid peer address format"); } @@ -47,7 +49,7 @@ pub fn client_command(client_args: &ArgMatches, global_config: GlobalConfig) { let peer = peer_args.value_of("peer").unwrap(); if let Ok(addr) = peer.parse() { - unban_peer(&server_config, &addr); + unban_peer(&server_config, &addr, api_secret); } else { panic!("Invalid peer address format"); } @@ -56,7 +58,7 @@ pub fn client_command(client_args: &ArgMatches, global_config: GlobalConfig) { } } -pub fn show_status(config: &ServerConfig) { +pub fn show_status(config: &ServerConfig, api_secret: Option) { println!(); let title = format!("Grin Server Status"); let mut t = term::stdout().unwrap(); @@ -65,7 +67,7 @@ pub fn show_status(config: &ServerConfig) { writeln!(t, "{}", title).unwrap(); writeln!(t, "--------------------------").unwrap(); t.reset().unwrap(); - match get_status_from_node(config) { + match get_status_from_node(config, api_secret) { Ok(status) => { writeln!(e, "Protocol version: {}", status.protocol_version).unwrap(); writeln!(e, "User agent: {}", status.user_agent).unwrap(); @@ -84,7 +86,7 @@ pub fn show_status(config: &ServerConfig) { println!() } -pub fn ban_peer(config: &ServerConfig, peer_addr: &SocketAddr) { +pub fn ban_peer(config: &ServerConfig, peer_addr: &SocketAddr, api_secret: Option) { let params = ""; let mut e = term::stdout().unwrap(); let url = format!( @@ -92,14 +94,14 @@ pub fn ban_peer(config: &ServerConfig, peer_addr: &SocketAddr) { config.api_http_addr, peer_addr.to_string() ); - match api::client::post_no_ret(url.as_str(), ¶ms).map_err(|e| Error::API(e)) { + match api::client::post_no_ret(url.as_str(), api_secret, ¶ms).map_err(|e| Error::API(e)) { Ok(_) => writeln!(e, "Successfully banned peer {}", peer_addr.to_string()).unwrap(), Err(_) => writeln!(e, "Failed to ban peer {}", peer_addr).unwrap(), }; e.reset().unwrap(); } -pub fn unban_peer(config: &ServerConfig, peer_addr: &SocketAddr) { +pub fn unban_peer(config: &ServerConfig, peer_addr: &SocketAddr, api_secret: Option) { let params = ""; let mut e = term::stdout().unwrap(); let url = format!( @@ -107,17 +109,23 @@ pub fn unban_peer(config: &ServerConfig, peer_addr: &SocketAddr) { config.api_http_addr, peer_addr.to_string() ); - match api::client::post_no_ret(url.as_str(), ¶ms).map_err(|e| Error::API(e)) { + let res: Result<(), api::Error>; + res = api::client::post_no_ret(url.as_str(), api_secret, ¶ms); + + match res.map_err(|e| Error::API(e)) { Ok(_) => writeln!(e, "Successfully unbanned peer {}", peer_addr).unwrap(), Err(_) => writeln!(e, "Failed to unban peer {}", peer_addr).unwrap(), }; e.reset().unwrap(); } -pub fn list_connected_peers(config: &ServerConfig) { +pub fn list_connected_peers(config: &ServerConfig, api_secret: Option) { let mut e = term::stdout().unwrap(); let url = format!("http://{}/v1/peers/connected", config.api_http_addr); - match api::client::get::>(url.as_str()).map_err(|e| Error::API(e)) { + let peers_info: Result, api::Error>; + peers_info = api::client::get::>(url.as_str(), api_secret); + + match peers_info.map_err(|e| Error::API(e)) { Ok(connected_peers) => { let mut index = 0; for connected_peer in connected_peers { @@ -137,9 +145,12 @@ pub fn list_connected_peers(config: &ServerConfig) { e.reset().unwrap(); } -fn get_status_from_node(config: &ServerConfig) -> Result { +fn get_status_from_node( + config: &ServerConfig, + api_secret: Option, +) -> Result { let url = format!("http://{}/v1/status", config.api_http_addr); - api::client::get::(url.as_str()).map_err(|e| Error::API(e)) + api::client::get::(url.as_str(), api_secret).map_err(|e| Error::API(e)) } /// Error type wrapping underlying module errors. diff --git a/src/bin/cmd/wallet.rs b/src/bin/cmd/wallet.rs index 791988a35..582fe9491 100644 --- a/src/bin/cmd/wallet.rs +++ b/src/bin/cmd/wallet.rs @@ -30,6 +30,7 @@ use grin_wallet::{self, controller, display, libwallet}; use grin_wallet::{HTTPWalletClient, LMDBBackend, WalletConfig, WalletInst, WalletSeed}; use keychain; use servers::start_webwallet_server; +use util::file::get_first_line; use util::LOGGER; pub fn _init_wallet_seed(wallet_config: WalletConfig) { @@ -129,6 +130,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { // Handle listener startup commands { let wallet = instantiate_wallet(wallet_config.clone(), passphrase); + let api_secret = get_first_line(wallet_config.api_secret_path.clone()); match wallet_args.subcommand() { ("listen", Some(listen_args)) => { if let Some(port) = listen_args.value_of("port") { @@ -143,22 +145,26 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) { }); } ("owner_api", Some(_api_args)) => { - controller::owner_listener(wallet, "127.0.0.1:13420").unwrap_or_else(|e| { - panic!( - "Error creating wallet api listener: {:?} Config: {:?}", - e, wallet_config - ) - }); + controller::owner_listener(wallet, "127.0.0.1:13420", api_secret).unwrap_or_else( + |e| { + panic!( + "Error creating wallet api listener: {:?} Config: {:?}", + e, wallet_config + ) + }, + ); } ("web", Some(_api_args)) => { // start owner listener and run static file server start_webwallet_server(); - controller::owner_listener(wallet, "127.0.0.1:13420").unwrap_or_else(|e| { - panic!( - "Error creating wallet api listener: {:?} Config: {:?}", - e, wallet_config - ) - }); + controller::owner_listener(wallet, "127.0.0.1:13420", api_secret).unwrap_or_else( + |e| { + panic!( + "Error creating wallet api listener: {:?} Config: {:?}", + e, wallet_config + ) + }, + ); } _ => {} }; diff --git a/util/Cargo.toml b/util/Cargo.toml index 4322ebe0d..396b4b3ff 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dependencies] backtrace = "0.3" +base64 = "0.9" byteorder = "1" lazy_static = "1" rand = "0.5" diff --git a/util/src/file.rs b/util/src/file.rs index 78a2ad51a..68b3d6c09 100644 --- a/util/src/file.rs +++ b/util/src/file.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. use std::fs; -use std::io; +use std::io::{self, BufRead}; use std::path::{Path, PathBuf}; use walkdir::WalkDir; @@ -70,3 +70,18 @@ fn copy_to(src: &Path, src_type: &fs::FileType, dst: &Path) -> io::Result { )); } } + +/// Retrieve first line from file +pub fn get_first_line(file_path: Option) -> Option { + match file_path { + Some(path) => match fs::File::open(path) { + Ok(file) => { + let buf_reader = io::BufReader::new(file); + let mut lines_iter = buf_reader.lines().map(|l| l.unwrap());; + lines_iter.next() + } + Err(_) => None, + }, + None => None, + } +} diff --git a/util/src/lib.rs b/util/src/lib.rs index 13a0caa21..aaa378a1d 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -22,6 +22,7 @@ #![warn(missing_docs)] extern crate backtrace; +extern crate base64; extern crate byteorder; extern crate rand; #[macro_use] @@ -117,3 +118,8 @@ pub fn kernel_sig_msg(fee: u64, lock_height: u64) -> [u8; 32] { BigEndian::write_u64(&mut bytes[24..], lock_height); bytes } + +/// Encode an utf8 string to a base64 string +pub fn to_base64(s: &str) -> String { + base64::encode(s) +} diff --git a/util/src/zip.rs b/util/src/zip.rs index ddcdb785e..b453e91c7 100644 --- a/util/src/zip.rs +++ b/util/src/zip.rs @@ -44,7 +44,8 @@ pub fn compress(src_dir: &Path, dst_file: &File) -> ZipResult<()> { for dent in it.filter_map(|e| e.ok()) { let path = dent.path(); - let name = path.strip_prefix(Path::new(src_dir)) + let name = path + .strip_prefix(Path::new(src_dir)) .unwrap() .to_str() .unwrap(); @@ -97,4 +98,4 @@ where } } Ok(()) -} \ No newline at end of file +} diff --git a/util/tests/file.rs b/util/tests/file.rs index b270034fc..b9af9ab40 100644 --- a/util/tests/file.rs +++ b/util/tests/file.rs @@ -13,27 +13,25 @@ // limitations under the License. extern crate grin_util as util; -extern crate walkdir; use std::fs::{self, File}; use std::io::{self, Write}; use std::path::Path; use util::file; -use walkdir::WalkDir; #[test] fn copy_dir() { let root = Path::new("./target/tmp2"); fs::create_dir_all(root.join("./original/sub")).unwrap(); fs::create_dir_all(root.join("./original/sub2")).unwrap(); - write_files("original".to_string(),&root).unwrap(); + write_files("original".to_string(), &root).unwrap(); let original_path = Path::new("./target/tmp2/original"); let copy_path = Path::new("./target/tmp2/copy"); file::copy_dir_to(original_path, copy_path).unwrap(); let original_files = file::list_files("./target/tmp2/original".to_string()); let copied_files = file::list_files("./target/tmp2/copy".to_string()); for i in 1..5 { - assert_eq!(copied_files[i],original_files[i]); + assert_eq!(copied_files[i], original_files[i]); } fs::remove_dir_all(root).unwrap(); } @@ -46,4 +44,4 @@ fn write_files(dir_name: String, root: &Path) -> io::Result<()> { let mut file = File::create(root.join(dir_name.clone() + "/sub/lorem"))?; file.write_all(b"Lorem ipsum dolor sit amet, consectetur adipiscing elit")?; Ok(()) -} \ No newline at end of file +} diff --git a/util/tests/zip.rs b/util/tests/zip.rs index 8330ab24b..65be45da4 100644 --- a/util/tests/zip.rs +++ b/util/tests/zip.rs @@ -13,13 +13,11 @@ // limitations under the License. extern crate grin_util as util; -extern crate walkdir; use std::fs::{self, File}; use std::io::{self, Write}; use std::path::Path; use util::zip; -use walkdir::WalkDir; #[test] fn zip_unzip() { @@ -27,7 +25,7 @@ fn zip_unzip() { let zip_name = "./target/tmp/zipped.zip"; fs::create_dir_all(root.join("./to_zip/sub")).unwrap(); - write_files("to_zip".to_string(),&root).unwrap(); + write_files("to_zip".to_string(), &root).unwrap(); let zip_file = File::create(zip_name).unwrap(); zip::compress(&root.join("./to_zip"), &zip_file).unwrap(); @@ -58,4 +56,4 @@ fn write_files(dir_name: String, root: &Path) -> io::Result<()> { let mut file = File::create(root.join(dir_name.clone() + "/sub/lorem"))?; file.write_all(b"Lorem ipsum dolor sit amet, consectetur adipiscing elit")?; Ok(()) -} \ No newline at end of file +} diff --git a/wallet/src/client.rs b/wallet/src/client.rs index ac3655654..5eb13823e 100644 --- a/wallet/src/client.rs +++ b/wallet/src/client.rs @@ -86,7 +86,7 @@ impl WalletClient for HTTPWalletClient { let url = format!("{}/v1/wallet/foreign/receive_tx", dest); debug!(LOGGER, "Posting transaction slate to {}", url); - let res = api::client::post(url.as_str(), slate).context( + let res = api::client::post(url.as_str(), None, slate).context( libwallet::ErrorKind::ClientCallback("Posting transaction slate"), )?; Ok(res) @@ -101,9 +101,9 @@ impl WalletClient for HTTPWalletClient { } else { url = format!("{}/v1/pool/push", dest); } - api::client::post_no_ret(url.as_str(), tx).context(libwallet::ErrorKind::ClientCallback( - "Posting transaction to node", - ))?; + api::client::post_no_ret(url.as_str(), None, tx).context( + libwallet::ErrorKind::ClientCallback("Posting transaction to node"), + )?; Ok(()) } @@ -111,7 +111,7 @@ impl WalletClient for HTTPWalletClient { fn get_chain_height(&self) -> Result { let addr = self.node_url(); let url = format!("{}/v1/chain", addr); - let res = api::client::get::(url.as_str()).context( + let res = api::client::get::(url.as_str(), None).context( libwallet::ErrorKind::ClientCallback("Getting chain height from node"), )?; Ok(res.height) @@ -136,7 +136,10 @@ impl WalletClient for HTTPWalletClient { for query_chunk in query_params.chunks(500) { let url = format!("{}/v1/chain/outputs/byids?{}", addr, query_chunk.join("&"),); - tasks.push(api::client::get_async::>(url.as_str())); + tasks.push(api::client::get_async::>( + url.as_str(), + None, + )); } let task = stream::futures_unordered(tasks).collect(); @@ -181,7 +184,7 @@ impl WalletClient for HTTPWalletClient { let mut api_outputs: Vec<(pedersen::Commitment, pedersen::RangeProof, bool, u64)> = Vec::new(); - match api::client::get::(url.as_str()) { + match api::client::get::(url.as_str(), None) { Ok(o) => { for out in o.outputs { let is_coinbase = match out.output_type { @@ -231,7 +234,7 @@ pub fn create_coinbase(dest: &str, block_fees: &BlockFees) -> Result Result { - let res = api::client::post(url, block_fees).context(ErrorKind::GenericError( + let res = api::client::post(url, None, block_fees).context(ErrorKind::GenericError( "Posting create coinbase".to_string(), ))?; Ok(res) diff --git a/wallet/src/libwallet/controller.rs b/wallet/src/libwallet/controller.rs index a84a9e755..eada5ff43 100644 --- a/wallet/src/libwallet/controller.rs +++ b/wallet/src/libwallet/controller.rs @@ -15,7 +15,7 @@ //! Controller for wallet.. instantiates and handles listeners (or single-run //! invocations) as needed. //! Still experimental -use api::{ApiServer, Handler, ResponseFuture, Router}; +use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router}; use core::core::Transaction; use failure::ResultExt; use futures::future::{err, ok}; @@ -36,7 +36,7 @@ use std::net::SocketAddr; use std::sync::{Arc, Mutex}; use url::form_urlencoded; use util::secp::pedersen; -use util::LOGGER; +use util::{to_base64, LOGGER}; /// Instantiate wallet Owner API for a single-use (command line) call /// Return a function containing a loaded API context to call @@ -66,7 +66,11 @@ where /// Listener version, providing same API but listening for requests on a /// port and wrapping the calls -pub fn owner_listener(wallet: Box, addr: &str) -> Result<(), Error> +pub fn owner_listener( + wallet: Box, + addr: &str, + api_secret: Option, +) -> Result<(), Error> where T: WalletBackend + Send + Sync + 'static, OwnerAPIHandler: Handler, @@ -77,6 +81,13 @@ where let api_handler = OwnerAPIHandler::new(wallet_arc); 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_realm = "Basic realm=GrinOwnerAPI".to_string(); + let basic_auth_middleware = Arc::new(BasicAuthMiddleware::new(api_basic_auth, basic_realm)); + router.add_middleware(basic_auth_middleware); + } router .add_route("/v1/wallet/owner/**", Arc::new(api_handler)) .map_err(|_| ErrorKind::GenericError("Router failed to add route".to_string()))?; @@ -91,7 +102,7 @@ where ))?; api_thread .join() - .map_err(|e| ErrorKind::GenericError(format!("API thread paniced :{:?}", e)).into()) + .map_err(|e| ErrorKind::GenericError(format!("API thread panicked :{:?}", e)).into()) } /// Listener version, providing same API but listening for requests on a diff --git a/wallet/src/types.rs b/wallet/src/types.rs index cf1de1866..8619998f6 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -39,6 +39,8 @@ pub struct WalletConfig { pub api_listen_interface: String, // The port this wallet will run on pub api_listen_port: u16, + /// Location of the secret for basic auth on the Owner API + pub api_secret_path: Option, // The api address of a running server node against which transaction inputs // will be checked during send pub check_node_api_http_addr: String, @@ -52,6 +54,7 @@ impl Default for WalletConfig { chain_type: Some(ChainTypes::Testnet3), api_listen_interface: "127.0.0.1".to_string(), api_listen_port: 13415, + api_secret_path: Some(".api_secret".to_string()), check_node_api_http_addr: "http://127.0.0.1:13413".to_string(), data_file_dir: ".".to_string(), }