// Copyright (c) 2009-2010 Satoshi Nakamoto // Copyright (c) 2009-2013 The Bitcoin Core developers // Copyright (c) 2016-2023 The Zcash developers // Distributed under the MIT software license, see the accompanying // file COPYING or https://www.opensource.org/licenses/mit-license.php . #include "chainparamsbase.h" #include "clientversion.h" #include "fs.h" #include "rpc/client.h" #include "rpc/protocol.h" #include "util/match.h" #include "util/system.h" #include "util/strencodings.h" #include #include #include #include "support/events.h" #include #ifdef WIN32 #include #else #include #include #include #endif const std::function G_TRANSLATION_FUN = nullptr; static const char DEFAULT_RPCCONNECT[] = "127.0.0.1"; static const int DEFAULT_HTTP_CLIENT_TIMEOUT=900; static const int CONTINUE_EXECUTION=-1; std::string HelpMessageCli() { std::string strUsage; strUsage += HelpMessageGroup(_("Options:")); strUsage += HelpMessageOpt("-?", _("This help message")); strUsage += HelpMessageOpt("-conf=", strprintf(_("Specify configuration file. Relative paths will be prefixed by datadir location. (default: %s)"), BITCOIN_CONF_FILENAME)); strUsage += HelpMessageOpt("-datadir=", _("Specify data directory (this path cannot use '~')")); strUsage += HelpMessageOpt("-stdin", _("Read extra arguments from standard input, one per line until EOF/Ctrl-D (recommended for sensitive information such as passphrases). If first extra argument is `walletpassphrase` then the first line(password) will not be echoed.")); AppendParamsHelpMessages(strUsage); strUsage += HelpMessageOpt("-rpcconnect=", strprintf(_("Send commands to node running on (default: %s)"), DEFAULT_RPCCONNECT)); strUsage += HelpMessageOpt("-rpcport=", strprintf(_("Connect to JSON-RPC on (default: %u or testnet: %u)"), 8232, 18232)); strUsage += HelpMessageOpt("-rpcwait", _("Wait for RPC server to start")); strUsage += HelpMessageOpt("-rpcuser=", _("Username for JSON-RPC connections")); strUsage += HelpMessageOpt("-rpcpassword=", _("Password for JSON-RPC connections")); strUsage += HelpMessageOpt("-rpcclienttimeout=", strprintf(_("Timeout in seconds during HTTP requests, or 0 for no timeout. (default: %d)"), DEFAULT_HTTP_CLIENT_TIMEOUT)); return strUsage; } ////////////////////////////////////////////////////////////////////////////// // // Start // // // Exception thrown on connection error. This error is used to determine // when to wait if -rpcwait is given. // class CConnectionFailed : public std::runtime_error { public: explicit inline CConnectionFailed(const std::string& msg) : std::runtime_error(msg) {} }; // // This function returns either one of EXIT_ codes when it's expected to stop the process or // CONTINUE_EXECUTION when it's expected to continue further. // static int AppInitRPC(int argc, char* argv[]) { static_assert(CONTINUE_EXECUTION != EXIT_FAILURE, "CONTINUE_EXECUTION should be different from EXIT_FAILURE"); static_assert(CONTINUE_EXECUTION != EXIT_SUCCESS, "CONTINUE_EXECUTION should be different from EXIT_SUCCESS"); // // Parameters // ParseParameters(argc, argv); if (argc<2 || mapArgs.count("-?") || mapArgs.count("-h") || mapArgs.count("-help") || mapArgs.count("-version")) { std::string strUsage = _("Zcash RPC client version") + " " + FormatFullVersion() + "\n" + PrivacyInfo(); if (!mapArgs.count("-version")) { strUsage += "\n" + _("Usage:") + "\n" + " zcash-cli [options] [params] " + _("Send command to Zcash") + "\n" + " zcash-cli [options] help " + _("List commands") + "\n" + " zcash-cli [options] help " + _("Get help for a command") + "\n"; strUsage += "\n" + HelpMessageCli(); } else { strUsage += LicenseInfo(); } fprintf(stdout, "%s", strUsage.c_str()); if (argc < 2) { fprintf(stderr, "Error: too few parameters\n"); return EXIT_FAILURE; } return EXIT_SUCCESS; } if (!fs::is_directory(GetDataDir(false))) { fprintf(stderr, "Error: Specified data directory \"%s\" does not exist.\n", mapArgs["-datadir"].c_str()); return EXIT_FAILURE; } try { ReadConfigFile(GetArg("-conf", BITCOIN_CONF_FILENAME), mapArgs, mapMultiArgs); } catch (const std::exception& e) { fprintf(stderr,"Error reading configuration file: %s\n", e.what()); return EXIT_FAILURE; } // Check for -testnet or -regtest parameter (BaseParams() calls are only valid after this clause) try { SelectBaseParams(ChainNameFromCommandLine()); } catch(std::exception &e) { fprintf(stderr, "Error: %s\n", e.what()); return EXIT_FAILURE; } if (GetBoolArg("-rpcssl", false)) { fprintf(stderr, "Error: SSL mode for RPC (-rpcssl) is no longer supported.\n"); return EXIT_FAILURE; } return CONTINUE_EXECUTION; } /** Reply structure for request_done to fill in */ struct HTTPReply { HTTPReply(): status(0), error(-1) {} int status; int error; std::string body; }; const char *http_errorstring(int code) { switch(code) { #if LIBEVENT_VERSION_NUMBER >= 0x02010300 case EVREQ_HTTP_TIMEOUT: return "timeout reached"; case EVREQ_HTTP_EOF: return "EOF reached"; case EVREQ_HTTP_INVALID_HEADER: return "error while reading header, or invalid header"; case EVREQ_HTTP_BUFFER_ERROR: return "error encountered while reading or writing"; case EVREQ_HTTP_REQUEST_CANCEL: return "request was canceled"; case EVREQ_HTTP_DATA_TOO_LONG: return "response body is larger than allowed"; #endif default: return "unknown"; } } static void http_request_done(struct evhttp_request *req, void *ctx) { HTTPReply *reply = static_cast(ctx); if (req == NULL) { /* If req is NULL, it means an error occurred while connecting: the * error code will have been passed to http_error_cb. */ reply->status = 0; return; } reply->status = evhttp_request_get_response_code(req); struct evbuffer *buf = evhttp_request_get_input_buffer(req); if (buf) { size_t size = evbuffer_get_length(buf); const char *data = (const char*)evbuffer_pullup(buf, size); if (data) reply->body = std::string(data, size); evbuffer_drain(buf, size); } } #if LIBEVENT_VERSION_NUMBER >= 0x02010300 static void http_error_cb(enum evhttp_request_error err, void *ctx) { HTTPReply *reply = static_cast(ctx); reply->error = err; } #endif tl::expected CallRPC(const std::string& strMethod, const UniValue& params); UniValue ConvertValues(const std::string &strMethod, const std::vector &strParams) { return RPCConvertValues(strMethod, strParams) .map_error([&](auto failure) { throw std::runtime_error( FormatConversionFailure(strMethod, failure) + examine(failure, match { [](const UnknownRPCMethod&) { return std::string{}; }, [&](const auto&) { auto helpMsg = CallRPC("help", ConvertValues("help", {strMethod})); return "\n\n" + (helpMsg.has_value() ? "Usage: " + helpMsg.value().get_str() : strprintf( "An error occurred while attempting to retrieve the " "help text for %s: %s", strMethod, helpMsg.error().get_str())); } })); }) .value(); } tl::expected CallRPC(const std::string& strMethod, const UniValue& params) { std::string host = GetArg("-rpcconnect", DEFAULT_RPCCONNECT); int port = GetArg("-rpcport", BaseParams().RPCPort()); // Obtain event base raii_event_base base = obtain_event_base(); // Synchronously look up hostname raii_evhttp_connection evcon = obtain_evhttp_connection_base(base.get(), host, port); evhttp_connection_set_timeout(evcon.get(), GetArg("-rpcclienttimeout", DEFAULT_HTTP_CLIENT_TIMEOUT)); HTTPReply response; raii_evhttp_request req = obtain_evhttp_request(http_request_done, (void*)&response); if (req == NULL) throw std::runtime_error("create http request failed"); #if LIBEVENT_VERSION_NUMBER >= 0x02010300 evhttp_request_set_error_cb(req.get(), http_error_cb); #endif // Get credentials std::string strRPCUserColonPass; if (mapArgs["-rpcpassword"] == "") { // Try fall back to cookie-based authentication if no password is provided if (!GetAuthCookie(&strRPCUserColonPass)) { throw std::runtime_error(strprintf( _("Could not locate RPC credentials. No authentication cookie could be found,\n" "and no rpcpassword is set in the configuration file (%s)."), GetConfigFile(GetArg("-conf", BITCOIN_CONF_FILENAME)).string().c_str())); } } else { strRPCUserColonPass = mapArgs["-rpcuser"] + ":" + mapArgs["-rpcpassword"]; } struct evkeyvalq* output_headers = evhttp_request_get_output_headers(req.get()); assert(output_headers); evhttp_add_header(output_headers, "Host", host.c_str()); evhttp_add_header(output_headers, "Connection", "close"); evhttp_add_header(output_headers, "Authorization", (std::string("Basic ") + EncodeBase64(strRPCUserColonPass)).c_str()); // Attach request data std::string strRequest = JSONRPCRequest(strMethod, params, 1); struct evbuffer* output_buffer = evhttp_request_get_output_buffer(req.get()); assert(output_buffer); evbuffer_add(output_buffer, strRequest.data(), strRequest.size()); int r = evhttp_make_request(evcon.get(), req.get(), EVHTTP_REQ_POST, "/"); req.release(); // ownership moved to evcon in above call if (r != 0) { throw CConnectionFailed("send http request failed"); } event_base_dispatch(base.get()); if (response.status == 0) throw CConnectionFailed(strprintf("couldn't connect to server: %s (code %d)\n(make sure server is running and you are connecting to the correct RPC port)", http_errorstring(response.error), response.error)); else if (response.status == HTTP_UNAUTHORIZED) throw std::runtime_error("incorrect rpcuser or rpcpassword (authorization failed)"); else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST && response.status != HTTP_NOT_FOUND && response.status != HTTP_INTERNAL_SERVER_ERROR) throw std::runtime_error(strprintf("server returned HTTP error %d", response.status)); else if (response.body.empty()) throw std::runtime_error("no response from server"); // Parse reply UniValue valReply(UniValue::VSTR); if (!valReply.read(response.body)) throw std::runtime_error("couldn't parse reply from server"); const UniValue& reply = valReply.get_obj(); if (reply.empty()) throw std::runtime_error("expected reply to have result, error and id properties"); const UniValue error = find_value(reply, "error"); return error.isNull() ? tl::expected(find_value(reply, "result")) : tl::make_unexpected(error); } bool SetStdinEcho(bool enable = true) { #ifdef WIN32 HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE); if (hStdin == INVALID_HANDLE_VALUE || hStdin == NULL) return false; DWORD mode; // if it's not a terminal then stdin won't be echoed, so that's not an error DWORD dwStdinType = GetFileType(hStdin) & ~FILE_TYPE_REMOTE; bool could_be_terminal = dwStdinType == FILE_TYPE_CHAR || (dwStdinType == FILE_TYPE_UNKNOWN && GetLastError() == NO_ERROR); if (!could_be_terminal || GetConsoleMode(hStdin, &mode) == 0) return true; if (!enable) mode &= ~ENABLE_ECHO_INPUT; else mode |= ENABLE_ECHO_INPUT; if (SetConsoleMode(hStdin, mode) == 0) return false; #else struct termios tty; if (tcgetattr(STDIN_FILENO, &tty) != 0) { // if it's not a terminal then stdin won't be echoed, so that's not an error return errno == ENOTTY; } if (!enable) tty.c_lflag &= ~ECHO; else tty.c_lflag |= ECHO; if (tcsetattr(STDIN_FILENO, TCSANOW, &tty) != 0) return false; #endif return true; } int CommandLineRPC(int argc, char *argv[]) { std::string strPrint; int nRet = 0; try { // Skip switches while (argc > 1 && IsSwitchChar(argv[1][0])) { argc--; argv++; } std::vector args = std::vector(&argv[1], &argv[argc]); if (GetBoolArg("-stdin", false)) { bool hide = false; if (args.size() > 0 && args[0] == "walletpassphrase") { if (!SetStdinEcho(false)) throw std::runtime_error("unable to set terminal to non-echoing"); hide = true; } // Read one arg per line from stdin and append std::string line; while (std::getline(std::cin,line)) { args.push_back(line); if (hide) { if (!SetStdinEcho(true)) throw std::runtime_error("unable to set terminal back to echoing"); hide = false; } } } if (args.size() < 1) throw std::runtime_error("too few parameters (need at least command)"); std::string strMethod = args[0]; auto params = ConvertValues(strMethod, std::vector(args.begin()+1, args.end())); // Execute and handle connection failures with -rpcwait const bool fWait = GetBoolArg("-rpcwait", false); do { try { auto reply = CallRPC(strMethod, params); if (!reply.has_value()) { auto error = reply.error(); // Error int code = error["code"].get_int(); if (fWait && code == RPC_IN_WARMUP) throw CConnectionFailed("server in warmup"); strPrint = "error: " + error.write(); nRet = abs(code); if (error.isObject()) { UniValue errCode = find_value(error, "code"); UniValue errMsg = find_value(error, "message"); strPrint = errCode.isNull() ? "" : "error code: "+errCode.getValStr()+"\n"; if (errMsg.isStr()) strPrint += "error message:\n"+errMsg.get_str(); } } else { auto result = reply.value(); // Result if (result.isNull()) strPrint = ""; else if (result.isStr()) strPrint = result.get_str(); else strPrint = result.write(2); } // Connection succeeded, no need to retry. break; } catch (const CConnectionFailed&) { if (fWait) MilliSleep(1000); else throw; } } while (fWait); } catch (const std::exception& e) { strPrint = std::string("error: ") + e.what(); nRet = EXIT_FAILURE; } catch (...) { PrintExceptionContinue(NULL, "CommandLineRPC()"); throw; } if (strPrint != "") { fprintf((nRet == 0 ? stdout : stderr), "%s\n", strPrint.c_str()); } return nRet; } int main(int argc, char* argv[]) { SetupEnvironment(); if (!SetupNetworking()) { fprintf(stderr, "Error: Initializing networking failed\n"); return EXIT_FAILURE; } try { int ret = AppInitRPC(argc, argv); if (ret != CONTINUE_EXECUTION) return ret; } catch (const std::exception& e) { PrintExceptionContinue(&e, "AppInitRPC()"); return EXIT_FAILURE; } catch (...) { PrintExceptionContinue(NULL, "AppInitRPC()"); return EXIT_FAILURE; } int ret = EXIT_FAILURE; try { ret = CommandLineRPC(argc, argv); } catch (const std::exception& e) { PrintExceptionContinue(&e, "CommandLineRPC()"); } catch (...) { PrintExceptionContinue(NULL, "CommandLineRPC()"); } return ret; }