diff --git a/Cargo.lock b/Cargo.lock
index aaf08d89..0d373484 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 00000000..3531ddd9
--- /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 adbb3c6f..42d0a34a 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 0c4a7537..41be1cf7 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 1fe66b7f..38cdd9d9 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 edf1604c..5f24b033 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 85eda3e8..1e3818d8 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 944a4327..ccc8579b 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 ed03b0f8..bea1c05a 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 9abb9387..7b1460cf 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 9b81e4d2..6ab4d99d 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 63e93005..22b6a509 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 9d2a5f7c..6c9c8eb3 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 5a7159c0..c3a36ea8 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 08a130a3..e09fc221 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 db24b651..19071806 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 2b0474ec..c1e61a70 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 714c9521..94c16c13 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 791988a3..582fe949 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 4322ebe0..396b4b3f 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 78a2ad51..68b3d6c0 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 13a0caa2..aaa378a1 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 ddcdb785..b453e91c 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 b270034f..b9af9ab4 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 8330ab24..65be45da 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 ac365565..5eb13823 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 a84a9e75..eada5ff4 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 cf1de186..8619998f 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(),
}