diff --git a/api/src/client.rs b/api/src/client.rs index bc176924c..98636ce0b 100644 --- a/api/src/client.rs +++ b/api/src/client.rs @@ -52,6 +52,15 @@ where } } +/// Helper function to easily issue a HTTP GET request +/// on a given URL that returns nothing. Handles request +/// building and response code checking. +pub fn get_no_ret(url: &str, api_secret: Option) -> Result<(), Error> { + let req = build_request(url, "GET", api_secret, None)?; + send_request(req)?; + Ok(()) +} + /// Helper function to easily issue a HTTP POST request with the provided JSON /// object as body on a given URL that returns a JSON object. Handles request /// building, JSON serialization and deserialization, and response code diff --git a/api/src/handlers/blocks_api.rs b/api/src/handlers/blocks_api.rs index e03d35c28..9da0a1c87 100644 --- a/api/src/handlers/blocks_api.rs +++ b/api/src/handlers/blocks_api.rs @@ -11,7 +11,6 @@ // 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 super::utils::{get_output, w}; use crate::chain; use crate::core::core::hash::Hash; @@ -68,10 +67,7 @@ impl HeaderHandler { impl Handler for HeaderHandler { fn get(&self, req: Request) -> ResponseFuture { - let el = match req.uri().path().trim_right_matches('/').rsplit('/').next() { - None => return response(StatusCode::BAD_REQUEST, "invalid url"), - Some(el) => el, - }; + let el = right_path_element!(req); result_to_response(self.get_header(el.to_string())) } } @@ -130,11 +126,7 @@ fn check_block_param(input: &String) -> Result<(), Error> { impl Handler for BlockHandler { fn get(&self, req: Request) -> ResponseFuture { - let el = match req.uri().path().trim_right_matches('/').rsplit('/').next() { - None => return response(StatusCode::BAD_REQUEST, "invalid url"), - Some(el) => el, - }; - + let el = right_path_element!(req); let h = match self.parse_input(el.to_string()) { Err(e) => { return response( diff --git a/api/src/handlers/chain_api.rs b/api/src/handlers/chain_api.rs index af269d71f..d0df2aa4f 100644 --- a/api/src/handlers/chain_api.rs +++ b/api/src/handlers/chain_api.rs @@ -22,9 +22,7 @@ use crate::util; use crate::util::secp::pedersen::Commitment; use crate::web::*; use hyper::{Body, Request, StatusCode}; -use std::collections::HashMap; use std::sync::Weak; -use url::form_urlencoded; /// Chain handler. Get the head details. /// GET /v1/chain @@ -101,21 +99,9 @@ impl OutputHandler { fn outputs_by_ids(&self, req: &Request) -> Result, Error> { let mut commitments: Vec = vec![]; - let query = match req.uri().query() { - Some(q) => q, - None => return Err(ErrorKind::RequestError("no query string".to_owned()))?, - }; - let params = form_urlencoded::parse(query.as_bytes()) - .into_owned() - .collect::>(); - - for (k, id) in params { - if k == "id" { - for id in id.split(',') { - commitments.push(id.to_owned()); - } - } - } + let query = must_get_query!(req); + let params = QueryParams::from(query); + params.process_multival_param("id", |id| commitments.push(id.to_owned())); let mut outputs: Vec = vec![]; for x in commitments { @@ -159,49 +145,17 @@ impl OutputHandler { // returns outputs for a specified range of blocks fn outputs_block_batch(&self, req: &Request) -> Result, Error> { let mut commitments: Vec = vec![]; - let mut start_height = 1; - let mut end_height = 1; - let mut include_rp = false; - let query = match req.uri().query() { - Some(q) => q, - None => return Err(ErrorKind::RequestError("no query string".to_owned()))?, - }; - - let params = form_urlencoded::parse(query.as_bytes()).into_owned().fold( - HashMap::new(), - |mut hm, (k, v)| { - hm.entry(k).or_insert(vec![]).push(v); - hm - }, - ); - - if let Some(ids) = params.get("id") { - for id in ids { - for id in id.split(',') { - if let Ok(x) = util::from_hex(String::from(id)) { - commitments.push(Commitment::from_vec(x)); - } - } + let query = must_get_query!(req); + let params = QueryParams::from(query); + params.process_multival_param("id", |id| { + if let Ok(x) = util::from_hex(String::from(id)) { + commitments.push(Commitment::from_vec(x)); } - } - if let Some(heights) = params.get("start_height") { - for height in heights { - start_height = height - .parse() - .map_err(|_| ErrorKind::RequestError("invalid start_height".to_owned()))?; - } - } - if let Some(heights) = params.get("end_height") { - for height in heights { - end_height = height - .parse() - .map_err(|_| ErrorKind::RequestError("invalid end_height".to_owned()))?; - } - } - if let Some(_) = params.get("include_rp") { - include_rp = true; - } + }); + let start_height = parse_param!(params, "start_height", 1); + let end_height = parse_param!(params, "end_height", 1); + let include_rp = params.get("include_rp").is_some(); debug!( "outputs_block_batch: {}-{}, {:?}, {:?}", @@ -223,11 +177,7 @@ impl OutputHandler { impl Handler for OutputHandler { fn get(&self, req: Request) -> ResponseFuture { - let command = match req.uri().path().trim_right_matches('/').rsplit('/').next() { - Some(c) => c, - None => return response(StatusCode::BAD_REQUEST, "invalid url"), - }; - match command { + match right_path_element!(req) { "byids" => result_to_response(self.outputs_by_ids(&req)), "byheight" => result_to_response(self.outputs_block_batch(&req)), _ => response(StatusCode::BAD_REQUEST, ""), diff --git a/api/src/handlers/peers_api.rs b/api/src/handlers/peers_api.rs index 31494a17c..692245dbc 100644 --- a/api/src/handlers/peers_api.rs +++ b/api/src/handlers/peers_api.rs @@ -37,11 +37,11 @@ pub struct PeersConnectedHandler { impl Handler for PeersConnectedHandler { fn get(&self, _req: Request) -> ResponseFuture { - let mut peers: Vec = vec![]; - for p in &w(&self.peers).connected_peers() { - let peer_info = p.info.clone(); - peers.push(peer_info.into()); - } + let peers: Vec = w(&self.peers) + .connected_peers() + .iter() + .map(|p| p.info.clone().into()) + .collect(); json_response(&peers) } } @@ -56,10 +56,7 @@ pub struct PeerHandler { impl Handler for PeerHandler { fn get(&self, req: Request) -> ResponseFuture { - let command = match req.uri().path().trim_right_matches('/').rsplit('/').next() { - Some(c) => c, - None => return response(StatusCode::BAD_REQUEST, "invalid url"), - }; + let command = right_path_element!(req); if let Ok(addr) = command.parse() { match w(&self.peers).get_peer(addr) { Ok(peer) => json_response(&peer), diff --git a/api/src/handlers/pool_api.rs b/api/src/handlers/pool_api.rs index f96d3685e..299b79a28 100644 --- a/api/src/handlers/pool_api.rs +++ b/api/src/handlers/pool_api.rs @@ -23,12 +23,11 @@ use crate::types::*; use crate::util; use crate::util::RwLock; use crate::web::*; +use failure::ResultExt; use futures::future::ok; use futures::Future; use hyper::{Body, Request, StatusCode}; -use std::collections::HashMap; use std::sync::Weak; -use url::form_urlencoded; /// Get basic information about the transaction pool. /// GET /v1/pool @@ -61,15 +60,7 @@ pub struct PoolPushHandler { impl PoolPushHandler { fn update_pool(&self, req: Request) -> Box + Send> { - let params = match req.uri().query() { - Some(query_string) => form_urlencoded::parse(query_string.as_bytes()) - .into_owned() - .fold(HashMap::new(), |mut hm, (k, v)| { - hm.entry(k).or_insert(vec![]).push(v); - hm - }), - None => HashMap::new(), - }; + let params = QueryParams::from(req.uri().query()); let fluff = params.get("fluff").is_some(); let pool_arc = w(&self.tx_pool).clone(); @@ -99,13 +90,14 @@ impl PoolPushHandler { // Push to tx pool. let mut tx_pool = pool_arc.write(); - let header = tx_pool.blockchain.chain_head().unwrap(); - tx_pool + let header = tx_pool + .blockchain + .chain_head() + .context(ErrorKind::Internal("Failed to get chain head".to_owned()))?; + let res = tx_pool .add_to_pool(source, tx, !fluff, &header) - .map_err(|e| { - error!("update_pool: failed with error: {:?}", e); - ErrorKind::Internal(format!("Failed to update pool: {:?}", e)).into() - }) + .context(ErrorKind::Internal("Failed to update pool".to_owned()))?; + Ok(res) }), ) } diff --git a/api/src/handlers/transactions_api.rs b/api/src/handlers/transactions_api.rs index 3a279b477..42723e71d 100644 --- a/api/src/handlers/transactions_api.rs +++ b/api/src/handlers/transactions_api.rs @@ -22,9 +22,7 @@ use crate::util::secp::pedersen::Commitment; use crate::web::*; use failure::ResultExt; use hyper::{Body, Request, StatusCode}; -use std::collections::HashMap; use std::sync::Weak; -use url::form_urlencoded; // Sum tree handler. Retrieve the roots: // GET /v1/txhashset/roots @@ -114,59 +112,14 @@ impl TxHashSetHandler { impl Handler for TxHashSetHandler { fn get(&self, req: Request) -> ResponseFuture { - let mut start_index = 1; - let mut max = 100; - let mut id = "".to_owned(); - // TODO: probably need to set a reasonable max limit here - let mut last_n = 10; - if let Some(query_string) = req.uri().query() { - let params = form_urlencoded::parse(query_string.as_bytes()) - .into_owned() - .fold(HashMap::new(), |mut hm, (k, v)| { - hm.entry(k).or_insert(vec![]).push(v); - hm - }); - if let Some(nums) = params.get("n") { - for num in nums { - if let Ok(n) = str::parse(num) { - last_n = n; - } - } - } - if let Some(start_indexes) = params.get("start_index") { - for si in start_indexes { - if let Ok(s) = str::parse(si) { - start_index = s; - } - } - } - if let Some(maxes) = params.get("max") { - for ma in maxes { - if let Ok(m) = str::parse(ma) { - max = m; - } - } - } - if let Some(ids) = params.get("id") { - if !ids.is_empty() { - id = ids.last().unwrap().to_owned(); - } - } - } - let command = match req - .uri() - .path() - .trim_right() - .trim_right_matches("/") - .rsplit("/") - .next() - { - Some(c) => c, - None => return response(StatusCode::BAD_REQUEST, "invalid url"), - }; + let params = QueryParams::from(req.uri().query()); + let last_n = parse_param_no_err!(params, "n", 10); + let start_index = parse_param_no_err!(params, "start_index", 1); + let max = parse_param_no_err!(params, "max", 100); + let id = parse_param_no_err!(params, "id", "".to_owned()); - match command { + match right_path_element!(req) { "roots" => json_response_pretty(&self.get_roots()), "lastoutputs" => json_response_pretty(&self.get_last_n_output(last_n)), "lastrangeproofs" => json_response_pretty(&self.get_last_n_rangeproof(last_n)), diff --git a/api/src/handlers/utils.rs b/api/src/handlers/utils.rs index 6a4f98ad0..bedaf0904 100644 --- a/api/src/handlers/utils.rs +++ b/api/src/handlers/utils.rs @@ -48,12 +48,15 @@ pub fn get_output( OutputIdentifier::new(OutputFeatures::Coinbase, &commit), ]; - for x in outputs.iter() { - if let Ok(_) = w(chain).is_unspent(&x) { - let block_height = w(chain).get_header_for_output(&x).unwrap().height; - let output_pos = w(chain).get_output_pos(&x.commit).unwrap_or(0); - return Ok((Output::new(&commit, block_height, output_pos), x.clone())); - } + for x in outputs.iter().filter(|x| w(chain).is_unspent(x).is_ok()) { + let block_height = w(chain) + .get_header_for_output(&x) + .context(ErrorKind::Internal( + "Can't get header for output".to_owned(), + ))? + .height; + let output_pos = w(chain).get_output_pos(&x.commit).unwrap_or(0); + return Ok((Output::new(&commit, block_height, output_pos), x.clone())); } Err(ErrorKind::NotFound)? } diff --git a/api/src/lib.rs b/api/src/lib.rs index 4ad2cebd8..282a29009 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -30,13 +30,14 @@ extern crate serde_derive; #[macro_use] extern crate log; +#[macro_use] +mod web; pub mod auth; pub mod client; mod handlers; mod rest; mod router; mod types; -mod web; pub use crate::auth::BasicAuthMiddleware; pub use crate::handlers::start_rest_apis; diff --git a/api/src/web.rs b/api/src/web.rs index 10eebcbb2..4d4a54ba5 100644 --- a/api/src/web.rs +++ b/api/src/web.rs @@ -5,7 +5,9 @@ use futures::{Future, Stream}; use hyper::{Body, Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json; +use std::collections::HashMap; use std::fmt::Debug; +use url::form_urlencoded; /// Parse request body pub fn parse_body(req: Request) -> Box + Send> @@ -81,3 +83,100 @@ pub fn just_response + Debug>(status: StatusCode, text: T) -> Resp pub fn response + Debug>(status: StatusCode, text: T) -> ResponseFuture { Box::new(ok(just_response(status, text))) } + +pub struct QueryParams { + params: HashMap>, +} + +impl QueryParams { + pub fn process_multival_param(&self, name: &str, mut f: F) + where + F: FnMut(&str), + { + if let Some(ids) = self.params.get(name) { + for id in ids { + for id in id.split(',') { + f(id); + } + } + } + } + + pub fn get(&self, name: &str) -> Option<&String> { + match self.params.get(name) { + None => None, + Some(v) => v.first(), + } + } +} + +impl From<&str> for QueryParams { + fn from(query_string: &str) -> Self { + let params = form_urlencoded::parse(query_string.as_bytes()) + .into_owned() + .fold(HashMap::new(), |mut hm, (k, v)| { + hm.entry(k).or_insert(vec![]).push(v); + hm + }); + QueryParams { params } + } +} + +impl From> for QueryParams { + fn from(query_string: Option<&str>) -> Self { + match query_string { + Some(query_string) => Self::from(query_string), + None => QueryParams { + params: HashMap::new(), + }, + } + } +} + +impl From> for QueryParams { + fn from(req: Request) -> Self { + Self::from(req.uri().query()) + } +} + +#[macro_export] +macro_rules! right_path_element( + ($req: expr) =>( + match $req.uri().path().trim_right_matches('/').rsplit('/').next() { + None => return response(StatusCode::BAD_REQUEST, "invalid url"), + Some(el) => el, + }; + )); + +#[macro_export] +macro_rules! must_get_query( + ($req: expr) =>( + match $req.uri().query() { + Some(q) => q, + None => return Err(ErrorKind::RequestError("no query string".to_owned()))?, + } + )); + +#[macro_export] +macro_rules! parse_param( + ($param: expr, $name: expr, $default: expr) =>( + match $param.get($name) { + None => $default, + Some(val) => match val.parse() { + Ok(val) => val, + Err(_) => return Err(ErrorKind::RequestError(format!("invalid value of parameter {}", $name)))?, + } + } + )); + +#[macro_export] +macro_rules! parse_param_no_err( + ($param: expr, $name: expr, $default: expr) =>( + match $param.get($name) { + None => $default, + Some(val) => match val.parse() { + Ok(val) => val, + Err(_) => $default, + } + } + )); diff --git a/servers/tests/api.rs b/servers/tests/api.rs index de856e82d..b2835911e 100644 --- a/servers/tests/api.rs +++ b/servers/tests/api.rs @@ -18,8 +18,7 @@ extern crate log; mod framework; use self::core::global::{self, ChainTypes}; -use self::util::init_test_logger; -use self::util::Mutex; +use self::util::{init_test_logger, to_hex, Mutex}; use crate::framework::{LocalServerContainer, LocalServerContainerConfig}; use grin_api as api; use grin_core as core; @@ -79,6 +78,7 @@ fn simple_server_wallet() { warn!("Testing chain handler"); let tip = get_tip(&base_addr, api_server_port); assert!(tip.is_ok()); + assert!(validate_chain(&base_addr, api_server_port).is_ok()); warn!("Testing status handler"); let status = get_status(&base_addr, api_server_port); @@ -94,17 +94,30 @@ fn simple_server_wallet() { warn!("Testing block handler"); let last_block_by_height = get_block_by_height(&base_addr, api_server_port, current_tip.height); assert!(last_block_by_height.is_ok()); + let block_hash = current_tip.last_block_pushed; let last_block_by_height_compact = get_block_by_height_compact(&base_addr, api_server_port, current_tip.height); assert!(last_block_by_height_compact.is_ok()); - let block_hash = current_tip.last_block_pushed; + let unspent_commit = get_unspent_output(&last_block_by_height.unwrap()).unwrap(); + let last_block_by_hash = get_block_by_hash(&base_addr, api_server_port, &block_hash); assert!(last_block_by_hash.is_ok()); let last_block_by_hash_compact = get_block_by_hash_compact(&base_addr, api_server_port, &block_hash); assert!(last_block_by_hash_compact.is_ok()); + warn!("Testing header handler"); + let last_header_by_height = + get_header_by_height(&base_addr, api_server_port, current_tip.height); + assert!(last_header_by_height.is_ok()); + + let last_header_by_hash = get_header_by_hash(&base_addr, api_server_port, &block_hash); + assert!(last_header_by_hash.is_ok()); + + let last_header_by_commit = get_header_by_commit(&base_addr, api_server_port, &unspent_commit); + assert!(last_header_by_commit.is_ok()); + warn!("Testing chain output handler"); let start_height = 0; let end_height = current_tip.height; @@ -286,6 +299,20 @@ fn get_block_by_hash( api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } +fn get_header_by_commit( + base_addr: &String, + api_server_port: u16, + commit: &api::PrintableCommitment, +) -> Result { + let url = format!( + "http://{}:{}/v1/headers/{}", + base_addr, + api_server_port, + to_hex(commit.to_vec()) + ); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + fn get_block_by_hash_compact( base_addr: &String, api_server_port: u16, @@ -298,6 +325,31 @@ fn get_block_by_hash_compact( api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) } +// Header handler functions +fn get_header_by_height( + base_addr: &String, + api_server_port: u16, + height: u64, +) -> Result { + let url = format!( + "http://{}:{}/v1/headers/{}", + base_addr, api_server_port, height + ); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + +fn get_header_by_hash( + base_addr: &String, + api_server_port: u16, + header_hash: &String, +) -> Result { + let url = format!( + "http://{}:{}/v1/headers/{}", + base_addr, api_server_port, header_hash + ); + api::client::get::(url.as_str(), None).map_err(|e| Error::API(e)) +} + // Chain output handler functions fn get_outputs_by_ids1( base_addr: &String, @@ -343,6 +395,11 @@ fn get_outputs_by_height( api::client::get::>(url.as_str(), None).map_err(|e| Error::API(e)) } +fn validate_chain(base_addr: &String, api_server_port: u16) -> Result<(), Error> { + let url = format!("http://{}:{}/v1/chain/validate", base_addr, api_server_port); + api::client::get_no_ret(url.as_str(), None).map_err(|e| Error::API(e)) +} + // TxHashSet handler functions fn get_txhashset_roots(base_addr: &String, api_server_port: u16) -> Result { let url = format!( @@ -412,19 +469,6 @@ fn get_txhashset_lastkernels( 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 -// outputs -fn get_ids_from_block_outputs(block_outputs: Vec) -> Vec { - let mut ids: Vec = Vec::new(); - for block_output in block_outputs { - let outputs = &block_output.outputs; - for output in outputs { - ids.push(util::to_hex(output.clone().commit.0.to_vec())); - } - } - ids.into_iter().take(100).collect() -} - pub fn ban_peer(base_addr: &String, api_server_port: u16, peer_addr: &String) -> Result<(), Error> { let url = format!( "http://{}:{}/v1/peers/{}/ban", @@ -477,6 +521,27 @@ pub fn get_all_peers( 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 +// outputs +fn get_ids_from_block_outputs(block_outputs: Vec) -> Vec { + let mut ids: Vec = Vec::new(); + for block_output in block_outputs { + let outputs = &block_output.outputs; + for output in outputs { + ids.push(util::to_hex(output.clone().commit.0.to_vec())); + } + } + ids.into_iter().take(100).collect() +} + +fn get_unspent_output(block: &api::BlockPrintable) -> Option { + match block.outputs.iter().find(|o| !o.spent) { + None => None, + Some(output) => Some(api::PrintableCommitment { + commit: output.commit.clone(), + }), + } +} /// Error type wrapping underlying module errors. #[derive(Debug)] pub enum Error {