提交 cbac9107 编写于 作者: M Matt Witherspoon

Add HTTPS support (in addition to HTTP) for cleos

Add HTTPS support for cleos when talking to nodeos and keosd. This removes the -H, -P, and --wallet-* options in favor of options that specify the server as an entire URL such as https://server:8888/
上级 fe6961f7
......@@ -12,7 +12,9 @@
#include <istream>
#include <ostream>
#include <string>
#include <regex>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <fc/variant.hpp>
#include <fc/io/json.hpp>
#include <eosio/chain/exceptions.hpp>
......@@ -22,132 +24,155 @@
using boost::asio::ip::tcp;
namespace eosio { namespace client { namespace http {
fc::variant call( const std::string& server, uint16_t port,
const std::string& path,
const fc::variant& postdata ) {
try {
std::string postjson;
if( !postdata.is_null() )
postjson = fc::json::to_string( postdata );
void do_connect(tcp::socket& sock, const std::string& server, const std::string& port) {
// Get a list of endpoints corresponding to the server name.
tcp::resolver resolver(sock.get_io_service());
tcp::resolver::query query(server, port);
boost::asio::connect(sock, resolver.resolve(query));
}
boost::asio::io_service io_service;
template<class T>
std::string do_txrx(T& socket, boost::asio::streambuf& request_buff, unsigned int& status_code) {
// Send the request.
boost::asio::write(socket, request_buff);
// Read the response status line. The response streambuf will automatically
// grow to accommodate the entire line. The growth may be limited by passing
// a maximum size to the streambuf constructor.
boost::asio::streambuf response;
boost::asio::read_until(socket, response, "\r\n");
// Check that response is OK.
std::istream response_stream(&response);
std::string http_version;
response_stream >> http_version;
response_stream >> status_code;
std::string status_message;
std::getline(response_stream, status_message);
FC_ASSERT( !(!response_stream || http_version.substr(0, 5) != "HTTP/"), "Invalid Response" );
// Read the response headers, which are terminated by a blank line.
boost::asio::read_until(socket, response, "\r\n\r\n");
// Process the response headers.
std::string header;
int response_content_length = -1;
std::regex clregex(R"xx(^content-length:\s+(\d+))xx", std::regex_constants::icase);
while (std::getline(response_stream, header) && header != "\r") {
std::smatch match;
if(std::regex_search(header, match, clregex))
response_content_length = std::stoi(match[1]);
}
FC_ASSERT(response_content_length >= 0, "Invalid content-length response");
// Get a list of endpoints corresponding to the server name.
tcp::resolver resolver(io_service);
tcp::resolver::query query(server, std::to_string(port) );
tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
tcp::resolver::iterator end;
while( endpoint_iterator != end ) {
// Try each endpoint until we successfully establish a connection.
tcp::socket socket(io_service);
try {
boost::asio::connect(socket, endpoint_iterator);
endpoint_iterator = end;
} catch( std::exception& e ) {
++endpoint_iterator;
if( endpoint_iterator != end ) {
continue;
} else {
throw connection_exception(fc::log_messages{
FC_LOG_MESSAGE( error, "Connection to ${server}:${port}${path} is refused",
("server", server)("port", port)("path", path) ),
FC_LOG_MESSAGE( error, e.what())
});
}
}
// Form the request. We specify the "Connection: close" header so that the
// server will close the socket after transmitting the response. This will
// allow us to treat all data up until the EOF as the content.
boost::asio::streambuf request;
std::ostream request_stream(&request);
request_stream << "POST " << path << " HTTP/1.0\r\n";
request_stream << "Host: " << server << "\r\n";
request_stream << "content-length: " << postjson.size() << "\r\n";
request_stream << "Accept: */*\r\n";
request_stream << "Connection: close\r\n\r\n";
request_stream << postjson;
// Send the request.
boost::asio::write(socket, request);
// Read the response status line. The response streambuf will automatically
// grow to accommodate the entire line. The growth may be limited by passing
// a maximum size to the streambuf constructor.
boost::asio::streambuf response;
boost::asio::read_until(socket, response, "\r\n");
// Check that response is OK.
std::istream response_stream(&response);
std::string http_version;
response_stream >> http_version;
unsigned int status_code;
response_stream >> status_code;
std::string status_message;
std::getline(response_stream, status_message);
FC_ASSERT( !(!response_stream || http_version.substr(0, 5) != "HTTP/"), "Invalid Response" );
// Read the response headers, which are terminated by a blank line.
boost::asio::read_until(socket, response, "\r\n\r\n");
// Process the response headers.
std::string header;
while (std::getline(response_stream, header) && header != "\r")
{
// std::cout << header << "\n";
}
// std::cout << "\n";
std::stringstream re;
// Write whatever content we already have to output.
if (response.size() > 0)
// std::cout << &response;
re << &response;
// Read until EOF, writing data to output as we go.
boost::system::error_code error;
while (boost::asio::read(socket, response,
boost::asio::transfer_at_least(1), error))
re << &response;
if (error != boost::asio::error::eof)
throw boost::system::system_error(error);
// std::cout << re.str() <<"\n";
const auto response_result = fc::json::from_string(re.str());
if( status_code == 200 || status_code == 201 || status_code == 202 ) {
return response_result;
} else if( status_code == 404 ) {
// Unknown endpoint
if (path.compare(0, chain_func_base.size(), chain_func_base) == 0) {
throw chain::missing_chain_api_plugin_exception(FC_LOG_MESSAGE(error, "Chain API plugin is not enabled"));
} else if (path.compare(0, wallet_func_base.size(), wallet_func_base) == 0) {
throw chain::missing_wallet_api_plugin_exception(FC_LOG_MESSAGE(error, "Wallet is not available"));
} else if (path.compare(0, account_history_func_base.size(), account_history_func_base) == 0) {
throw chain::missing_account_history_api_plugin_exception(FC_LOG_MESSAGE(error, "Account History API plugin is not enabled"));
} else if (path.compare(0, net_func_base.size(), net_func_base) == 0) {
throw chain::missing_net_api_plugin_exception(FC_LOG_MESSAGE(error, "Net API plugin is not enabled"));
}
} else {
auto &&error_info = response_result.as<eosio::error_results>().error;
// Construct fc exception from error
const auto &error_details = error_info.details;
fc::log_messages logs;
for (auto itr = error_details.begin(); itr != error_details.end(); itr++) {
const auto& context = fc::log_context(fc::log_level::error, itr->file.data(), itr->line_number, itr->method.data());
logs.emplace_back(fc::log_message(context, itr->message));
}
throw fc::exception(logs, error_info.code, error_info.name, error_info.what);
}
FC_ASSERT( status_code == 200, "Error code ${c}\n: ${msg}\n", ("c", status_code)("msg", re.str()) );
std::stringstream re;
// Write whatever content we already have to output.
response_content_length -= response.size();
if (response.size() > 0)
re << &response;
boost::asio::read(socket, response, boost::asio::transfer_exactly(response_content_length));
re << &response;
return re.str();
}
fc::variant call( const std::string& server_url,
const std::string& path,
const fc::variant& postdata ) {
std::string postjson;
if( !postdata.is_null() )
postjson = fc::json::to_string( postdata );
boost::asio::io_service io_service;
string scheme, server, port, path_prefix;
//via rfc3986 and modified a bit to suck out the port number
//Sadly this doesn't work for ipv6 addresses
std::regex rgx(R"xx(^(([^:/?#]+):)?(//([^:/?#]*)(:(\d+))?)?([^?#]*)(\?([^#]*))?(#(.*))?)xx");
std::smatch match;
if(std::regex_search(server_url.begin(), server_url.end(), match, rgx)) {
scheme = match[2];
server = match[4];
port = match[6];
path_prefix = match[7];
}
if(scheme != "http" && scheme != "https")
FC_THROW("Unreconized URL scheme (${s}) in URL \"${u}\"", ("s", scheme)("u", server_url));
if(server.empty())
FC_THROW("No server parsed from URL \"${u}\"", ("u", server_url));
if(port.empty())
port = scheme == "http" ? "8888" : "443";
boost::trim_right_if(path_prefix, boost::is_any_of("/"));
boost::asio::streambuf request;
std::ostream request_stream(&request);
request_stream << "POST " << path_prefix + path << " HTTP/1.0\r\n";
request_stream << "Host: " << server << "\r\n";
request_stream << "content-length: " << postjson.size() << "\r\n";
request_stream << "Accept: */*\r\n";
request_stream << "Connection: close\r\n\r\n";
request_stream << postjson;
unsigned int status_code;
std::string re;
if(scheme == "http") {
tcp::socket socket(io_service);
do_connect(socket, server, port);
re = do_txrx(socket, request, status_code);
}
else { //https
boost::asio::ssl::context ssl_context(boost::asio::ssl::context::sslv23_client);
#if defined( __APPLE__ )
//TODO: this is undocumented/not supported; fix with keychain based approach
ssl_context.load_verify_file("/private/etc/ssl/cert.pem");
#elif defined( _WIN32 )
FC_THROW("HTTPS on Windows not supported");
#else
ssl_context.set_default_verify_paths();
#endif
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket(io_service, ssl_context);
socket.set_verify_mode(boost::asio::ssl::verify_peer);
do_connect(socket.next_layer(), server, port);
socket.handshake(boost::asio::ssl::stream_base::client);
re = do_txrx(socket, request, status_code);
//try and do a clean shutdown; but swallow if this fails (other side could have already gave TCP the ax)
try {socket.shutdown();} catch(...) {}
}
const auto response_result = fc::json::from_string(re);
if( status_code == 200 || status_code == 201 || status_code == 202 ) {
return response_result;
} else if( status_code == 404 ) {
// Unknown endpoint
if (path.compare(0, chain_func_base.size(), chain_func_base) == 0) {
throw chain::missing_chain_api_plugin_exception(FC_LOG_MESSAGE(error, "Chain API plugin is not enabled"));
} else if (path.compare(0, wallet_func_base.size(), wallet_func_base) == 0) {
throw chain::missing_wallet_api_plugin_exception(FC_LOG_MESSAGE(error, "Wallet is not available"));
} else if (path.compare(0, account_history_func_base.size(), account_history_func_base) == 0) {
throw chain::missing_account_history_api_plugin_exception(FC_LOG_MESSAGE(error, "Account History API plugin is not enabled"));
} else if (path.compare(0, net_func_base.size(), net_func_base) == 0) {
throw chain::missing_net_api_plugin_exception(FC_LOG_MESSAGE(error, "Net API plugin is not enabled"));
}
} else {
auto &&error_info = response_result.as<eosio::error_results>().error;
// Construct fc exception from error
const auto &error_details = error_info.details;
fc::log_messages logs;
for (auto itr = error_details.begin(); itr != error_details.end(); itr++) {
const auto& context = fc::log_context(fc::log_level::error, itr->file.data(), itr->line_number, itr->method.data());
logs.emplace_back(fc::log_message(context, itr->message));
}
FC_ASSERT( !"unable to connect" );
} FC_CAPTURE_AND_RETHROW() // error, "Request Path: ${server}:${port}${path}\nRequest Post Data: ${postdata}" ,
// ("server", server)("port", port)("path", path)("postdata", postdata) )
throw fc::exception(logs, error_info.code, error_info.name, error_info.what);
}
FC_ASSERT( status_code == 200, "Error code ${c}\n: ${msg}\n", ("c", status_code)("msg", re) );
return response_result;
}
}}}
......@@ -5,7 +5,7 @@
#pragma once
namespace eosio { namespace client { namespace http {
fc::variant call( const std::string& server, uint16_t port,
fc::variant call( const std::string& server_url,
const std::string& path,
const fc::variant& postdata = fc::variant() );
......
......@@ -22,11 +22,10 @@ Usage: programs/cleos/cleos [OPTIONS] SUBCOMMAND
Options:
-h,--help Print this help message and exit
-H,--host TEXT=localhost the host where nodeos is running
-p,--port UINT=8888 the port where nodeos is running
--wallet-host TEXT=localhost
the host where keosd is running
--wallet-port UINT=8888 the port where keosd is running
-u,--url TEXT=http://localhost:8888/
the http/https URL where nodeos is running
--wallet-url TEXT=http://localhost:8888/
the http/https URL where keosd is running
-v,--verbose output verbose actions on error
Subcommands:
......@@ -39,7 +38,8 @@ Subcommands:
wallet Interact with local wallet
sign Sign a transaction
push Push arbitrary transactions to the blockchain
multisig Multisig contract commands
```
To get help with any particular subcommand, run it with no arguments as well:
```
......@@ -134,13 +134,8 @@ FC_DECLARE_EXCEPTION( localized_exception, 10000000, "an error occured" );
FC_MULTILINE_MACRO_END \
)
string program = "eosc";
string host = "localhost";
uint32_t port = 8888;
// restricting use of wallet to localhost
string wallet_host = "localhost";
uint32_t wallet_port = 8888;
string url = "http://localhost:8888/";
string wallet_url = "http://localhost:8888/";
auto tx_expiration = fc::seconds(30);
string tx_ref_block_num_or_id;
......@@ -194,16 +189,27 @@ vector<chain::permission_level> get_account_permissions(const vector<string>& pe
}
template<typename T>
fc::variant call( const std::string& server, uint16_t port,
fc::variant call( const std::string& url,
const std::string& path,
const T& v ) { return eosio::client::http::call( server, port, path, fc::variant(v) ); }
const T& v ) {
try {
return eosio::client::http::call( url, path, fc::variant(v) );
}
catch(boost::system::system_error& e) {
if(url == ::url)
std::cerr << localized("Failed to connect to nodeos at ${u}; is nodeos running?", ("u", url)) << std::endl;
else if(url == ::wallet_url)
std::cerr << localized("Failed to connect to keosd at ${u}; is keosd running?", ("u", url)) << std::endl;
throw connection_exception(fc::log_messages{FC_LOG_MESSAGE(error, e.what())});
}
}
template<typename T>
fc::variant call( const std::string& path,
const T& v ) { return eosio::client::http::call( host, port, path, fc::variant(v) ); }
const T& v ) { return ::call( url, path, fc::variant(v) ); }
eosio::chain_apis::read_only::get_info_results get_info() {
return call(host, port, get_info_func ).as<eosio::chain_apis::read_only::get_info_results>();
return ::call(url, get_info_func, fc::variant()).as<eosio::chain_apis::read_only::get_info_results>();
}
string generate_nonce_value() {
......@@ -228,18 +234,18 @@ chain::action generate_nonce() {
fc::variant determine_required_keys(const signed_transaction& trx) {
// TODO better error checking
//wdump((trx));
const auto& public_keys = call(wallet_host, wallet_port, wallet_public_keys);
const auto& public_keys = call(wallet_url, wallet_public_keys);
auto get_arg = fc::mutable_variant_object
("transaction", (transaction)trx)
("available_keys", public_keys);
const auto& required_keys = call(host, port, get_required_keys, get_arg);
const auto& required_keys = call(get_required_keys, get_arg);
return required_keys["required_keys"];
}
void sign_transaction(signed_transaction& trx, fc::variant& required_keys) {
// TODO determine chain id
fc::variants sign_args = {fc::variant(trx), required_keys, fc::variant(chain_id_type{})};
const auto& signed_trx = call(wallet_host, wallet_port, wallet_sign_trx, sign_args);
const auto& signed_trx = call(wallet_url, wallet_sign_trx, sign_args);
trx = signed_trx.as<signed_transaction>();
}
......@@ -528,6 +534,13 @@ struct set_action_permission_subcommand {
}
};
CLI::callback_t old_host_port = [](CLI::results_t) {
std::cerr << localized("Host and port options (-H, --wallet-host, etc.) have been replaced with -u/--url and --wallet-url\n"
"Use for example -u http://localhost:8888 or --url https://example.invalid/\n");
exit(1);
return false;
};
int main( int argc, char** argv ) {
fc::path binPath = argv[0];
if (binPath.is_relative()) {
......@@ -540,10 +553,13 @@ int main( int argc, char** argv ) {
CLI::App app{"Command Line Interface to EOSIO Client"};
app.require_subcommand();
app.add_option( "-H,--host", host, localized("the host where nodeos is running"), true );
app.add_option( "-p,--port", port, localized("the port where nodeos is running"), true );
app.add_option( "--wallet-host", wallet_host, localized("the host where keosd is running"), true );
app.add_option( "--wallet-port", wallet_port, localized("the port where keosd is running"), true );
app.add_option( "-H,--host", old_host_port, localized("the host where nodeos is running") )->group("hidden");
app.add_option( "-p,--port", old_host_port, localized("the port where nodeos is running") )->group("hidden");
app.add_option( "--wallet-host", old_host_port, localized("the host where keosd is running") )->group("hidden");
app.add_option( "--wallet-port", old_host_port, localized("the port where keosd is running") )->group("hidden");
app.add_option( "-u,--url", url, localized("the http/https URL where nodeos is running"), true );
app.add_option( "--wallet-url", wallet_url, localized("the http/https URL where keosd is running"), true );
bool verbose_errors = false;
app.add_flag( "-v,--verbose", verbose_errors, localized("output verbose actions on error"));
......@@ -923,27 +939,27 @@ int main( int argc, char** argv ) {
auto connect = net->add_subcommand("connect", localized("start a new connection to a peer"), false);
connect->add_option("host", new_host, localized("The hostname:port to connect to."))->required();
connect->set_callback([&] {
const auto& v = call(host, port, net_connect, new_host);
const auto& v = call(net_connect, new_host);
std::cout << fc::json::to_pretty_string(v) << std::endl;
});
auto disconnect = net->add_subcommand("disconnect", localized("close an existing connection"), false);
disconnect->add_option("host", new_host, localized("The hostname:port to disconnect from."))->required();
disconnect->set_callback([&] {
const auto& v = call(host, port, net_disconnect, new_host);
const auto& v = call(net_disconnect, new_host);
std::cout << fc::json::to_pretty_string(v) << std::endl;
});
auto status = net->add_subcommand("status", localized("status of existing connection"), false);
status->add_option("host", new_host, localized("The hostname:port to query status of connection"))->required();
status->set_callback([&] {
const auto& v = call(host, port, net_status, new_host);
const auto& v = call(net_status, new_host);
std::cout << fc::json::to_pretty_string(v) << std::endl;
});
auto connections = net->add_subcommand("peers", localized("status of all existing peers"), false);
connections->set_callback([&] {
const auto& v = call(host, port, net_connections, new_host);
const auto& v = call(net_connections, new_host);
std::cout << fc::json::to_pretty_string(v) << std::endl;
});
......@@ -957,7 +973,7 @@ int main( int argc, char** argv ) {
auto createWallet = wallet->add_subcommand("create", localized("Create a new wallet locally"), false);
createWallet->add_option("-n,--name", wallet_name, localized("The name of the new wallet"), true);
createWallet->set_callback([&wallet_name] {
const auto& v = call(wallet_host, wallet_port, wallet_create, wallet_name);
const auto& v = call(wallet_url, wallet_create, wallet_name);
std::cout << localized("Creating wallet: ${wallet_name}", ("wallet_name", wallet_name)) << std::endl;
std::cout << localized("Save password to use in the future to unlock this wallet.") << std::endl;
std::cout << localized("Without password imported keys will not be retrievable.") << std::endl;
......@@ -968,8 +984,7 @@ int main( int argc, char** argv ) {
auto openWallet = wallet->add_subcommand("open", localized("Open an existing wallet"), false);
openWallet->add_option("-n,--name", wallet_name, localized("The name of the wallet to open"));
openWallet->set_callback([&wallet_name] {
/*const auto& v = */call(wallet_host, wallet_port, wallet_open, wallet_name);
//std::cout << fc::json::to_pretty_string(v) << std::endl;
call(wallet_url, wallet_open, wallet_name);
std::cout << localized("Opened: ${wallet_name}", ("wallet_name", wallet_name)) << std::endl;
});
......@@ -977,17 +992,14 @@ int main( int argc, char** argv ) {
auto lockWallet = wallet->add_subcommand("lock", localized("Lock wallet"), false);
lockWallet->add_option("-n,--name", wallet_name, localized("The name of the wallet to lock"));
lockWallet->set_callback([&wallet_name] {
/*const auto& v = */call(wallet_host, wallet_port, wallet_lock, wallet_name);
call(wallet_url, wallet_lock, wallet_name);
std::cout << localized("Locked: ${wallet_name}", ("wallet_name", wallet_name)) << std::endl;
//std::cout << fc::json::to_pretty_string(v) << std::endl;
});
// lock all wallets
auto locakAllWallets = wallet->add_subcommand("lock_all", localized("Lock all unlocked wallets"), false);
locakAllWallets->set_callback([] {
/*const auto& v = */call(wallet_host, wallet_port, wallet_lock_all);
//std::cout << fc::json::to_pretty_string(v) << std::endl;
call(wallet_url, wallet_lock_all);
std::cout << localized("Locked All Wallets") << std::endl;
});
......@@ -1006,9 +1018,8 @@ int main( int argc, char** argv ) {
fc::variants vs = {fc::variant(wallet_name), fc::variant(wallet_pw)};
/*const auto& v = */call(wallet_host, wallet_port, wallet_unlock, vs);
call(wallet_url, wallet_unlock, vs);
std::cout << localized("Unlocked: ${wallet_name}", ("wallet_name", wallet_name)) << std::endl;
//std::cout << fc::json::to_pretty_string(v) << std::endl;
});
// import keys into wallet
......@@ -1026,23 +1037,22 @@ int main( int argc, char** argv ) {
public_key_type pubkey = wallet_key.get_public_key();
fc::variants vs = {fc::variant(wallet_name), fc::variant(wallet_key)};
const auto& v = call(wallet_host, wallet_port, wallet_import_key, vs);
call(wallet_url, wallet_import_key, vs);
std::cout << localized("imported private key for: ${pubkey}", ("pubkey", std::string(pubkey))) << std::endl;
//std::cout << fc::json::to_pretty_string(v) << std::endl;
});
// list wallets
auto listWallet = wallet->add_subcommand("list", localized("List opened wallets, * = unlocked"), false);
listWallet->set_callback([] {
std::cout << localized("Wallets:") << std::endl;
const auto& v = call(wallet_host, wallet_port, wallet_list);
const auto& v = call(wallet_url, wallet_list);
std::cout << fc::json::to_pretty_string(v) << std::endl;
});
// list keys
auto listKeys = wallet->add_subcommand("keys", localized("List of private keys from all unlocked wallets in wif format."), false);
listKeys->set_callback([] {
const auto& v = call(wallet_host, wallet_port, wallet_list_keys);
const auto& v = call(wallet_url, wallet_list_keys);
std::cout << fc::json::to_pretty_string(v) << std::endl;
});
......@@ -1415,18 +1425,9 @@ int main( int argc, char** argv ) {
} catch (const explained_exception& e) {
return 1;
} catch (connection_exception& e) {
auto errorString = e.to_detail_string();
if (errorString.find(fc::json::to_string(port)) != string::npos) {
std::cerr << localized("Failed to connect to nodeos at ${ip}:${port}; is nodeos running?", ("ip", host)("port", port)) << std::endl;
} else if (errorString.find(fc::json::to_string(wallet_port)) != string::npos) {
std::cerr << localized("Failed to connect to keosd at ${ip}:${port}; is keosd running?", ("ip", wallet_host)("port", wallet_port)) << std::endl;
} else {
std::cerr << localized("Failed to connect") << std::endl;
}
if (verbose_errors) {
elog("connect error: ${e}", ("e", errorString));
}
if (verbose_errors) {
elog("connect error: ${e}", ("e", e.to_detail_string()));
}
} catch (const fc::exception& e) {
// attempt to extract the error code if one is present
if (!print_recognized_errors(e, verbose_errors)) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册