Refactor API handlers (#2572)

Also add some API tests
This commit is contained in:
hashmap 2019-02-15 22:17:00 +01:00 committed by GitHub
parent aad0e9402a
commit ac6ed71abd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 236 additions and 175 deletions

View file

@ -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<String>) -> 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

View file

@ -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<Body>) -> 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<Body>) -> 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(

View file

@ -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<Body>) -> Result<Vec<Output>, Error> {
let mut commitments: Vec<String> = 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::<Vec<(String, String)>>();
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<Output> = 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<Body>) -> Result<Vec<BlockOutputs>, Error> {
let mut commitments: Vec<Commitment> = 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<Body>) -> 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, ""),

View file

@ -37,11 +37,11 @@ pub struct PeersConnectedHandler {
impl Handler for PeersConnectedHandler {
fn get(&self, _req: Request<Body>) -> ResponseFuture {
let mut peers: Vec<PeerInfoDisplay> = vec![];
for p in &w(&self.peers).connected_peers() {
let peer_info = p.info.clone();
peers.push(peer_info.into());
}
let peers: Vec<PeerInfoDisplay> = 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<Body>) -> 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),

View file

@ -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<Body>) -> Box<dyn Future<Item = (), Error = Error> + 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)
}),
)
}

View file

@ -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<Body>) -> 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)),

View file

@ -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)?
}

View file

@ -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;

View file

@ -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<T>(req: Request<Body>) -> Box<dyn Future<Item = T, Error = Error> + Send>
@ -81,3 +83,100 @@ pub fn just_response<T: Into<Body> + Debug>(status: StatusCode, text: T) -> Resp
pub fn response<T: Into<Body> + Debug>(status: StatusCode, text: T) -> ResponseFuture {
Box::new(ok(just_response(status, text)))
}
pub struct QueryParams {
params: HashMap<String, Vec<String>>,
}
impl QueryParams {
pub fn process_multival_param<F>(&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<Option<&str>> 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<Request<Body>> for QueryParams {
fn from(req: Request<Body>) -> 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,
}
}
));

View file

@ -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::<api::BlockPrintable>(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<api::BlockHeaderPrintable, Error> {
let url = format!(
"http://{}:{}/v1/headers/{}",
base_addr,
api_server_port,
to_hex(commit.to_vec())
);
api::client::get::<api::BlockHeaderPrintable>(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::<api::CompactBlockPrintable>(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<api::BlockHeaderPrintable, Error> {
let url = format!(
"http://{}:{}/v1/headers/{}",
base_addr, api_server_port, height
);
api::client::get::<api::BlockHeaderPrintable>(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<api::BlockHeaderPrintable, Error> {
let url = format!(
"http://{}:{}/v1/headers/{}",
base_addr, api_server_port, header_hash
);
api::client::get::<api::BlockHeaderPrintable>(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::<Vec<api::BlockOutputs>>(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<api::TxHashSet, Error> {
let url = format!(
@ -412,19 +469,6 @@ fn get_txhashset_lastkernels(
api::client::get::<Vec<api::TxHashSetNode>>(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<api::BlockOutputs>) -> Vec<String> {
let mut ids: Vec<String> = 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::<Vec<p2p::PeerData>>(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<api::BlockOutputs>) -> Vec<String> {
let mut ids: Vec<String> = 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<api::PrintableCommitment> {
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 {