diff --git a/programs/cleos/httpc.cpp b/programs/cleos/httpc.cpp index 98426e98d7cb63af73c73a1bbb78aaa1b038a2b8..e043f70dcca4f8e1517031f1e8178dfa88123a08 100644 --- a/programs/cleos/httpc.cpp +++ b/programs/cleos/httpc.cpp @@ -12,7 +12,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -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 + 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().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 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().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; } }}} diff --git a/programs/cleos/httpc.hpp b/programs/cleos/httpc.hpp index ffe0cda08a42e63e616ac0f0f26ab647c3be80db..31870bf3937d18de65d1bee6fda1885a8725ac37 100644 --- a/programs/cleos/httpc.hpp +++ b/programs/cleos/httpc.hpp @@ -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() ); diff --git a/programs/cleos/main.cpp b/programs/cleos/main.cpp index 9cd1713fc984b5cce08b4b62f6d7d9307fdebc92..31106d5b07c8dd3c61443ff3eddd27e1f422e425 100644 --- a/programs/cleos/main.cpp +++ b/programs/cleos/main.cpp @@ -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 get_account_permissions(const vector& pe } template -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 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(); + return ::call(url, get_info_func, fc::variant()).as(); } 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(); } @@ -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)) {