diff --git a/api/src/json_rpc.rs b/api/src/json_rpc.rs new file mode 100644 index 0000000000..fb6a8e434f --- /dev/null +++ b/api/src/json_rpc.rs @@ -0,0 +1,276 @@ +// Copyright 2020 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. +// Derived from https://github.com/apoelstra/rust-jsonrpc + +//! JSON RPC Client functionality +use std::{error, fmt}; + +use hyper; +use serde::{Deserialize, Serialize}; + +/// Builds a request +pub fn build_request<'a, 'b>(name: &'a str, params: &'b serde_json::Value) -> Request<'a, 'b> { + Request { + method: name, + params: params, + id: From::from(1), + jsonrpc: Some("2.0"), + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +/// A JSONRPC request object +pub struct Request<'a, 'b> { + /// The name of the RPC call + pub method: &'a str, + /// Parameters to the RPC call + pub params: &'b serde_json::Value, + /// Identifier for this Request, which should appear in the response + pub id: serde_json::Value, + /// jsonrpc field, MUST be "2.0" + pub jsonrpc: Option<&'a str>, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +/// A JSONRPC response object +pub struct Response { + /// A result if there is one, or null + pub result: Option, + /// An error if there is one, or null + pub error: Option, + /// Identifier for this Request, which should match that of the request + pub id: serde_json::Value, + /// jsonrpc field, MUST be "2.0" + pub jsonrpc: Option, +} + +impl Response { + /// Extract the result from a response + pub fn result(&self) -> Result { + if let Some(ref e) = self.error { + return Err(Error::Rpc(e.clone())); + } + + let result = match self.result.clone() { + Some(r) => serde_json::from_value(r["Ok"].clone()).map_err(Error::Json), + None => serde_json::from_value(serde_json::Value::Null).map_err(Error::Json), + }?; + Ok(result) + } + + /// Extract the result from a response, consuming the response + pub fn into_result(self) -> Result { + if let Some(e) = self.error { + return Err(Error::Rpc(e)); + } + self.result() + } + + /// Return the RPC error, if there was one, but do not check the result + pub fn _check_error(self) -> Result<(), Error> { + if let Some(e) = self.error { + Err(Error::Rpc(e)) + } else { + Ok(()) + } + } + + /// Returns whether or not the `result` field is empty + pub fn _is_none(&self) -> bool { + self.result.is_none() + } +} + +/// A library error +#[derive(Debug)] +pub enum Error { + /// Json error + Json(serde_json::Error), + /// Client error + Hyper(hyper::error::Error), + /// Error response + Rpc(RpcError), + /// Response to a request did not have the expected nonce + _NonceMismatch, + /// Response to a request had a jsonrpc field other than "2.0" + _VersionMismatch, + /// Batches can't be empty + _EmptyBatch, + /// Too many responses returned in batch + _WrongBatchResponseSize, + /// Batch response contained a duplicate ID + _BatchDuplicateResponseId(serde_json::Value), + /// Batch response contained an ID that didn't correspond to any request ID + _WrongBatchResponseId(serde_json::Value), +} + +impl From for Error { + fn from(e: serde_json::Error) -> Error { + Error::Json(e) + } +} + +impl From for Error { + fn from(e: hyper::error::Error) -> Error { + Error::Hyper(e) + } +} + +impl From for Error { + fn from(e: RpcError) -> Error { + Error::Rpc(e) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::Json(ref e) => write!(f, "JSON decode error: {}", e), + Error::Hyper(ref e) => write!(f, "Hyper error: {}", e), + Error::Rpc(ref r) => write!(f, "RPC error response: {:?}", r), + Error::_BatchDuplicateResponseId(ref v) => { + write!(f, "duplicate RPC batch response ID: {}", v) + } + Error::_WrongBatchResponseId(ref v) => write!(f, "wrong RPC batch response ID: {}", v), + _ => f.write_str(&self.to_string()), + } + } +} + +impl std::error::Error for Error { + fn description(&self) -> &str { + match *self { + Error::Json(_) => "JSON decode error", + Error::Hyper(_) => "Hyper error", + Error::Rpc(_) => "RPC error response", + Error::_NonceMismatch => "Nonce of response did not match nonce of request", + Error::_VersionMismatch => "`jsonrpc` field set to non-\"2.0\"", + Error::_EmptyBatch => "batches can't be empty", + Error::_WrongBatchResponseSize => "too many responses returned in batch", + Error::_BatchDuplicateResponseId(_) => "batch response contained a duplicate ID", + Error::_WrongBatchResponseId(_) => { + "batch response contained an ID that didn't correspond to any request ID" + } + } + } + + fn cause(&self) -> Option<&dyn error::Error> { + match *self { + Error::Json(ref e) => Some(e), + Error::Hyper(ref e) => Some(e), + _ => None, + } + } +} + +/// Standard error responses, as described at at +/// http://www.jsonrpc.org/specification#error_object +/// +/// # Documentation Copyright +/// Copyright (C) 2007-2010 by the JSON-RPC Working Group +/// +/// This document and translations of it may be used to implement JSON-RPC, it +/// may be copied and furnished to others, and derivative works that comment +/// on or otherwise explain it or assist in its implementation may be prepared, +/// copied, published and distributed, in whole or in part, without restriction +/// of any kind, provided that the above copyright notice and this paragraph +/// are included on all such copies and derivative works. However, this document +/// itself may not be modified in any way. +/// +/// The limited permissions granted above are perpetual and will not be revoked. +/// +/// This document and the information contained herein is provided "AS IS" and +/// ALL WARRANTIES, EXPRESS OR IMPLIED are DISCLAIMED, INCLUDING BUT NOT LIMITED +/// TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY +/// RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A +/// PARTICULAR PURPOSE. +/// +#[allow(dead_code)] +#[derive(Debug)] +pub enum StandardError { + /// Invalid JSON was received by the server. + /// An error occurred on the server while parsing the JSON text. + ParseError, + /// The JSON sent is not a valid Request object. + InvalidRequest, + /// The method does not exist / is not available. + MethodNotFound, + /// Invalid method parameter(s). + InvalidParams, + /// Internal JSON-RPC error. + InternalError, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +/// A JSONRPC error object +pub struct RpcError { + /// The integer identifier of the error + pub code: i32, + /// A string describing the error + pub message: String, + /// Additional data specific to the error + pub data: Option, +} + +/// Create a standard error responses +pub fn _standard_error(code: StandardError, data: Option) -> RpcError { + match code { + StandardError::ParseError => RpcError { + code: -32700, + message: "Parse error".to_string(), + data: data, + }, + StandardError::InvalidRequest => RpcError { + code: -32600, + message: "Invalid Request".to_string(), + data: data, + }, + StandardError::MethodNotFound => RpcError { + code: -32601, + message: "Method not found".to_string(), + data: data, + }, + StandardError::InvalidParams => RpcError { + code: -32602, + message: "Invalid params".to_string(), + data: data, + }, + StandardError::InternalError => RpcError { + code: -32603, + message: "Internal error".to_string(), + data: data, + }, + } +} + +/// Converts a Rust `Result` to a JSONRPC response object +pub fn _result_to_response( + result: Result, + id: serde_json::Value, +) -> Response { + match result { + Ok(data) => Response { + result: Some(data), + error: None, + id: id, + jsonrpc: Some(String::from("2.0")), + }, + Err(err) => Response { + result: None, + error: Some(err), + id: id, + jsonrpc: Some(String::from("2.0")), + }, + } +} diff --git a/api/src/lib.rs b/api/src/lib.rs index 3f7d3221cb..0b3f788f18 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -38,11 +38,12 @@ pub mod client; mod foreign; mod foreign_rpc; mod handlers; +pub mod json_rpc; mod owner; mod owner_rpc; mod rest; mod router; -mod types; +pub mod types; pub use crate::auth::{ BasicAuthMiddleware, BasicAuthURIMiddleware, GRIN_BASIC_REALM, GRIN_FOREIGN_BASIC_REALM, diff --git a/src/bin/cmd/client.rs b/src/bin/cmd/client.rs index 796062d91d..dcb2f14171 100644 --- a/src/bin/cmd/client.rs +++ b/src/bin/cmd/client.rs @@ -17,30 +17,158 @@ use std::net::SocketAddr; use clap::ArgMatches; -use crate::api; +use crate::api::client; +use crate::api::json_rpc::*; +use crate::api::types::Status; use crate::config::GlobalConfig; -use crate::p2p; -use crate::servers::ServerConfig; +use crate::p2p::types::PeerInfoDisplay; use crate::util::file::get_first_line; +use serde_json::json; use term; +const ENDPOINT: &str = "/v2/owner"; + +#[derive(Clone)] +pub struct HTTPNodeClient { + node_url: String, + node_api_secret: Option, +} +impl HTTPNodeClient { + /// Create a new client that will communicate with the given grin node + pub fn new(node_url: &str, node_api_secret: Option) -> HTTPNodeClient { + HTTPNodeClient { + node_url: node_url.to_owned(), + node_api_secret: node_api_secret, + } + } + fn send_json_request( + &self, + method: &str, + params: &serde_json::Value, + ) -> Result { + let url = format!("http://{}{}", self.node_url, ENDPOINT); + let req = build_request(method, params); + let res = + client::post::(url.as_str(), self.node_api_secret.clone(), &req); + + match res { + Err(e) => { + let report = format!("Error calling {}: {}", method, e); + error!("{}", report); + Err(Error::RPCError(report)) + } + Ok(inner) => match inner.clone().into_result() { + Ok(r) => Ok(r), + Err(e) => { + error!("{:?}", inner); + let report = format!("Unable to parse response for {}: {}", method, e); + error!("{}", report); + Err(Error::RPCError(report)) + } + }, + } + } + + pub fn show_status(&self) { + println!(); + let title = "Grin Server Status".to_string(); + if term::stdout().is_none() { + println!("Could not open terminal"); + return; + } + let mut t = term::stdout().unwrap(); + let mut e = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + writeln!(t, "--------------------------").unwrap(); + t.reset().unwrap(); + match self.send_json_request::("get_status", &serde_json::Value::Null) { + Ok(status) => { + writeln!(e, "Protocol version: {:?}", status.protocol_version).unwrap(); + writeln!(e, "User agent: {}", status.user_agent).unwrap(); + writeln!(e, "Connections: {}", status.connections).unwrap(); + writeln!(e, "Chain height: {}", status.tip.height).unwrap(); + writeln!(e, "Last block hash: {}", status.tip.last_block_pushed).unwrap(); + writeln!(e, "Previous block hash: {}", status.tip.prev_block_to_last).unwrap(); + writeln!(e, "Total difficulty: {}", status.tip.total_difficulty).unwrap(); + writeln!(e, "Sync status: {}", status.sync_status).unwrap(); + if let Some(sync_info) = status.sync_info { + writeln!(e, "Sync info: {}", sync_info).unwrap(); + } + } + Err(_) => writeln!( + e, + "WARNING: Client failed to get data. Is your `grin server` offline or broken?" + ) + .unwrap(), + }; + e.reset().unwrap(); + println!() + } + + pub fn list_connected_peers(&self) { + let mut e = term::stdout().unwrap(); + match self.send_json_request::>( + "get_connected_peers", + &serde_json::Value::Null, + ) { + Ok(connected_peers) => { + for (index, connected_peer) in connected_peers.into_iter().enumerate() { + writeln!(e, "Peer {}:", index).unwrap(); + writeln!(e, "Capabilities: {:?}", connected_peer.capabilities).unwrap(); + writeln!(e, "User agent: {}", connected_peer.user_agent).unwrap(); + writeln!(e, "Version: {:?}", connected_peer.version).unwrap(); + writeln!(e, "Peer address: {}", connected_peer.addr).unwrap(); + writeln!(e, "Height: {}", connected_peer.height).unwrap(); + writeln!(e, "Total difficulty: {}", connected_peer.total_difficulty).unwrap(); + writeln!(e, "Direction: {:?}", connected_peer.direction).unwrap(); + println!(); + } + } + Err(_) => writeln!(e, "Failed to get connected peers").unwrap(), + }; + e.reset().unwrap(); + } + + pub fn ban_peer(&self, peer_addr: &SocketAddr) { + let mut e = term::stdout().unwrap(); + let params = json!([peer_addr]); + match self.send_json_request::<()>("ban_peer", ¶ms) { + Ok(_) => writeln!(e, "Successfully banned peer {}", peer_addr).unwrap(), + Err(_) => writeln!(e, "Failed to ban peer {}", peer_addr).unwrap(), + }; + e.reset().unwrap(); + } + + pub fn unban_peer(&self, peer_addr: &SocketAddr) { + let mut e = term::stdout().unwrap(); + let params = json!([peer_addr]); + match self.send_json_request::<()>("unban_peer", ¶ms) { + Ok(_) => writeln!(e, "Successfully unbanned peer {}", peer_addr).unwrap(), + Err(_) => writeln!(e, "Failed to unban peer {}", peer_addr).unwrap(), + }; + e.reset().unwrap(); + } +} + pub fn client_command(client_args: &ArgMatches<'_>, global_config: GlobalConfig) -> i32 { // 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()); + let node_client = HTTPNodeClient::new(&server_config.api_http_addr, api_secret.clone()); match client_args.subcommand() { ("status", Some(_)) => { - show_status(&server_config, api_secret); + node_client.show_status(); } ("listconnectedpeers", Some(_)) => { - list_connected_peers(&server_config, api_secret); + node_client.list_connected_peers(); } ("ban", Some(peer_args)) => { let peer = peer_args.value_of("peer").unwrap(); if let Ok(addr) = peer.parse() { - ban_peer(&server_config, &addr, api_secret); + node_client.ban_peer(&addr); } else { panic!("Invalid peer address format"); } @@ -49,7 +177,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, api_secret); + node_client.unban_peer(&addr); } else { panic!("Invalid peer address format"); } @@ -58,111 +186,9 @@ pub fn client_command(client_args: &ArgMatches<'_>, global_config: GlobalConfig) } 0 } - -pub fn show_status(config: &ServerConfig, api_secret: Option) { - println!(); - let title = "Grin Server Status".to_string(); - if term::stdout().is_none() { - println!("Could not open terminal"); - return; - } - let mut t = term::stdout().unwrap(); - let mut e = term::stdout().unwrap(); - t.fg(term::color::MAGENTA).unwrap(); - writeln!(t, "{}", title).unwrap(); - writeln!(t, "--------------------------").unwrap(); - t.reset().unwrap(); - 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(); - writeln!(e, "Connections: {}", status.connections).unwrap(); - writeln!(e, "Chain height: {}", status.tip.height).unwrap(); - writeln!(e, "Last block hash: {}", status.tip.last_block_pushed).unwrap(); - writeln!(e, "Previous block hash: {}", status.tip.prev_block_to_last).unwrap(); - writeln!(e, "Total difficulty: {}", status.tip.total_difficulty).unwrap(); - } - Err(_) => writeln!( - e, - "WARNING: Client failed to get data. Is your `grin server` offline or broken?" - ) - .unwrap(), - }; - e.reset().unwrap(); - println!() -} - -pub fn ban_peer(config: &ServerConfig, peer_addr: &SocketAddr, api_secret: Option) { - let params = ""; - let mut e = term::stdout().unwrap(); - let url = format!( - "http://{}/v1/peers/{}/ban", - config.api_http_addr, - peer_addr.to_string() - ); - match api::client::post_no_ret(url.as_str(), api_secret, ¶ms).map_err(Error::API) { - 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, api_secret: Option) { - let params = ""; - let mut e = term::stdout().unwrap(); - let url = format!( - "http://{}/v1/peers/{}/unban", - config.api_http_addr, - peer_addr.to_string() - ); - let res: Result<(), api::Error>; - res = api::client::post_no_ret(url.as_str(), api_secret, ¶ms); - - match res.map_err(Error::API) { - 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, api_secret: Option) { - let mut e = term::stdout().unwrap(); - let url = format!("http://{}/v1/peers/connected", config.api_http_addr); - // let peers_info: Result, api::Error>; - - let peers_info = api::client::get::>(url.as_str(), api_secret); - - match peers_info.map_err(Error::API) { - Ok(connected_peers) => { - for (index, connected_peer) in connected_peers.into_iter().enumerate() { - writeln!(e, "Peer {}:", index).unwrap(); - writeln!(e, "Capabilities: {:?}", connected_peer.capabilities).unwrap(); - writeln!(e, "User agent: {}", connected_peer.user_agent).unwrap(); - writeln!(e, "Version: {:?}", connected_peer.version).unwrap(); - writeln!(e, "Peer address: {}", connected_peer.addr).unwrap(); - writeln!(e, "Height: {}", connected_peer.height).unwrap(); - writeln!(e, "Total difficulty: {}", connected_peer.total_difficulty).unwrap(); - writeln!(e, "Direction: {:?}", connected_peer.direction).unwrap(); - println!(); - } - } - Err(_) => writeln!(e, "Failed to get connected peers").unwrap(), - }; - - e.reset().unwrap(); -} - -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(), api_secret).map_err(Error::API) -} - /// Error type wrapping underlying module errors. #[derive(Debug)] enum Error { - /// Error originating from HTTP API calls. - API(api::Error), + /// RPC Error + RPCError(String), }