diff options
Diffstat (limited to 'src/qt/rpcconsole.cpp')
-rw-r--r-- | src/qt/rpcconsole.cpp | 198 |
1 files changed, 148 insertions, 50 deletions
diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index 47af6a5724..38fe659c33 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -31,6 +31,7 @@ #include <QKeyEvent> #include <QMenu> +#include <QMessageBox> #include <QScrollBar> #include <QSettings> #include <QSignalMapper> @@ -63,6 +64,20 @@ const struct { {NULL, NULL} }; +namespace { + +// don't add private key handling cmd's to the history +const QStringList historyFilter = QStringList() + << "importprivkey" + << "importmulti" + << "signmessagewithprivkey" + << "signrawtransaction" + << "walletpassphrase" + << "walletpassphrasechange" + << "encryptwallet"; + +} + /* Object for executing console RPC commands in a separate thread. */ class RPCExecutor : public QObject @@ -113,10 +128,10 @@ public: #include "rpcconsole.moc" /** - * Split shell command line into a list of arguments and execute the command(s). + * Split shell command line into a list of arguments and optionally execute the command(s). * Aims to emulate \c bash and friends. * - * - Command nesting is possible with brackets [example: validateaddress(getnewaddress())] + * - Command nesting is possible with parenthesis; for example: validateaddress(getnewaddress()) * - Arguments are delimited with whitespace or comma * - Extra whitespace at the beginning and end and between arguments will be ignored * - Text can be "double" or 'single' quoted @@ -127,9 +142,11 @@ public: * * @param[out] result stringified Result from the executed command(chain) * @param[in] strCommand Command line to split + * @param[in] fExecute set true if you want the command to be executed + * @param[out] pstrFilteredOut Command line, filtered to remove any sensitive data */ -bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string &strCommand) +bool RPCConsole::RPCParseCommandLine(std::string &strResult, const std::string &strCommand, const bool fExecute, std::string * const pstrFilteredOut) { std::vector< std::vector<std::string> > stack; stack.push_back(std::vector<std::string>()); @@ -137,6 +154,8 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string enum CmdParseState { STATE_EATING_SPACES, + STATE_EATING_SPACES_IN_ARG, + STATE_EATING_SPACES_IN_BRACKETS, STATE_ARGUMENT, STATE_SINGLEQUOTED, STATE_DOUBLEQUOTED, @@ -147,12 +166,35 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string } state = STATE_EATING_SPACES; std::string curarg; UniValue lastResult; + unsigned nDepthInsideSensitive = 0; + size_t filter_begin_pos = 0, chpos; + std::vector<std::pair<size_t, size_t>> filter_ranges; + + auto add_to_current_stack = [&](const std::string& curarg) { + if (stack.back().empty() && (!nDepthInsideSensitive) && historyFilter.contains(QString::fromStdString(curarg), Qt::CaseInsensitive)) { + nDepthInsideSensitive = 1; + filter_begin_pos = chpos; + } + stack.back().push_back(curarg); + }; + + auto close_out_params = [&]() { + if (nDepthInsideSensitive) { + if (!--nDepthInsideSensitive) { + assert(filter_begin_pos); + filter_ranges.push_back(std::make_pair(filter_begin_pos, chpos)); + filter_begin_pos = 0; + } + } + stack.pop_back(); + }; std::string strCommandTerminated = strCommand; if (strCommandTerminated.back() != '\n') strCommandTerminated += "\n"; - for(char ch: strCommandTerminated) + for (chpos = 0; chpos < strCommandTerminated.size(); ++chpos) { + char ch = strCommandTerminated[chpos]; switch(state) { case STATE_COMMAND_EXECUTED_INNER: @@ -171,7 +213,7 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string curarg += ch; break; } - if (curarg.size()) + if (curarg.size() && fExecute) { // if we have a value query, query arrays with index and objects with a string key UniValue subelement; @@ -196,7 +238,7 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string breakParsing = false; // pop the stack and return the result to the current command arguments - stack.pop_back(); + close_out_params(); // don't stringify the json in case of a string to avoid doublequotes if (lastResult.isStr()) @@ -208,7 +250,7 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string if (curarg.size()) { if (stack.size()) - stack.back().push_back(curarg); + add_to_current_stack(curarg); else strResult = curarg; } @@ -220,6 +262,8 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string break; } case STATE_ARGUMENT: // In or after argument + case STATE_EATING_SPACES_IN_ARG: + case STATE_EATING_SPACES_IN_BRACKETS: case STATE_EATING_SPACES: // Handle runs of whitespace switch(ch) { @@ -227,41 +271,55 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string case '\'': state = STATE_SINGLEQUOTED; break; case '\\': state = STATE_ESCAPE_OUTER; break; case '(': case ')': case '\n': + if (state == STATE_EATING_SPACES_IN_ARG) + throw std::runtime_error("Invalid Syntax"); if (state == STATE_ARGUMENT) { if (ch == '(' && stack.size() && stack.back().size() > 0) - stack.push_back(std::vector<std::string>()); - if (curarg.size()) { - // don't allow commands after executed commands on baselevel - if (!stack.size()) - throw std::runtime_error("Invalid Syntax"); - stack.back().push_back(curarg); + if (nDepthInsideSensitive) { + ++nDepthInsideSensitive; + } + stack.push_back(std::vector<std::string>()); } + + // don't allow commands after executed commands on baselevel + if (!stack.size()) + throw std::runtime_error("Invalid Syntax"); + + add_to_current_stack(curarg); curarg.clear(); - state = STATE_EATING_SPACES; + state = STATE_EATING_SPACES_IN_BRACKETS; } if ((ch == ')' || ch == '\n') && stack.size() > 0) { - std::string strPrint; - // Convert argument list to JSON objects in method-dependent way, - // and pass it along with the method name to the dispatcher. - JSONRPCRequest req; - req.params = RPCConvertValues(stack.back()[0], std::vector<std::string>(stack.back().begin() + 1, stack.back().end())); - req.strMethod = stack.back()[0]; - lastResult = tableRPC.execute(req); + if (fExecute) { + // Convert argument list to JSON objects in method-dependent way, + // and pass it along with the method name to the dispatcher. + JSONRPCRequest req; + req.params = RPCConvertValues(stack.back()[0], std::vector<std::string>(stack.back().begin() + 1, stack.back().end())); + req.strMethod = stack.back()[0]; + lastResult = tableRPC.execute(req); + } state = STATE_COMMAND_EXECUTED; curarg.clear(); } break; case ' ': case ',': case '\t': - if(state == STATE_ARGUMENT) // Space ends argument + if(state == STATE_EATING_SPACES_IN_ARG && curarg.empty() && ch == ',') + throw std::runtime_error("Invalid Syntax"); + + else if(state == STATE_ARGUMENT) // Space ends argument { - if (curarg.size()) - stack.back().push_back(curarg); + add_to_current_stack(curarg); curarg.clear(); } + if ((state == STATE_EATING_SPACES_IN_BRACKETS || state == STATE_ARGUMENT) && ch == ',') + { + state = STATE_EATING_SPACES_IN_ARG; + break; + } state = STATE_EATING_SPACES; break; default: curarg += ch; state = STATE_ARGUMENT; @@ -291,6 +349,16 @@ bool RPCConsole::RPCExecuteCommandLine(std::string &strResult, const std::string break; } } + if (pstrFilteredOut) { + if (STATE_COMMAND_EXECUTED == state) { + assert(!stack.empty()); + close_out_params(); + } + *pstrFilteredOut = strCommand; + for (auto i = filter_ranges.rbegin(); i != filter_ranges.rend(); ++i) { + pstrFilteredOut->replace(i->first, i->second - i->first, "(…)"); + } + } switch(state) // final state { case STATE_COMMAND_EXECUTED: @@ -382,7 +450,6 @@ RPCConsole::RPCConsole(const PlatformStyle *_platformStyle, QWidget *parent) : // based timer interface RPCSetTimerInterfaceIfUnset(rpcTimerInterface); - startExecutor(); setTrafficGraphRange(INITIAL_TRAFFIC_GRAPH_MINS); ui->detailWidget->hide(); @@ -396,7 +463,6 @@ RPCConsole::RPCConsole(const PlatformStyle *_platformStyle, QWidget *parent) : RPCConsole::~RPCConsole() { GUIUtil::saveWindowGeometry("nRPCConsoleWindow", this); - Q_EMIT stopExecutor(); RPCUnsetTimerInterface(rpcTimerInterface); delete rpcTimerInterface; delete ui; @@ -486,7 +552,7 @@ void RPCConsole::setClientModel(ClientModel *model) QAction* banAction365d = new QAction(tr("Ban for") + " " + tr("1 &year"), this); // create peer table context menu - peersTableContextMenu = new QMenu(); + peersTableContextMenu = new QMenu(this); peersTableContextMenu->addAction(disconnectAction); peersTableContextMenu->addAction(banAction1h); peersTableContextMenu->addAction(banAction24h); @@ -517,7 +583,7 @@ void RPCConsole::setClientModel(ClientModel *model) // peer table signal handling - update peer details when new nodes are added to the model connect(model->getPeerTableModel(), SIGNAL(layoutChanged()), this, SLOT(peerLayoutChanged())); // peer table signal handling - cache selected node ids - connect(model->getPeerTableModel(), SIGNAL(layoutAboutToChange()), this, SLOT(peerLayoutAboutToChange())); + connect(model->getPeerTableModel(), SIGNAL(layoutAboutToBeChanged()), this, SLOT(peerLayoutAboutToChange())); // set up ban table ui->banlistWidget->setModel(model->getBanTableModel()); @@ -534,7 +600,7 @@ void RPCConsole::setClientModel(ClientModel *model) QAction* unbanAction = new QAction(tr("&Unban"), this); // create ban table context menu - banTableContextMenu = new QMenu(); + banTableContextMenu = new QMenu(this); banTableContextMenu->addAction(unbanAction); // ban table context menu signals @@ -565,6 +631,14 @@ void RPCConsole::setClientModel(ClientModel *model) autoCompleter = new QCompleter(wordList, this); ui->lineEdit->setCompleter(autoCompleter); autoCompleter->popup()->installEventFilter(this); + // Start thread to execute RPC commands. + startExecutor(); + } + if (!model) { + // Client model is being set to 0, this means shutdown() is about to be called. + // Make sure we clean up the executor thread + Q_EMIT stopExecutor(); + thread.wait(); } } @@ -644,13 +718,18 @@ void RPCConsole::clear(bool clearHistory) "td.message { font-family: %1; font-size: %2; white-space:pre-wrap; } " "td.cmd-request { color: #006060; } " "td.cmd-error { color: red; } " + ".secwarning { color: red; }" "b { color: #006060; } " ).arg(fixedFontInfo.family(), QString("%1pt").arg(consoleFontSize)) ); message(CMD_REPLY, (tr("Welcome to the %1 RPC console.").arg(tr(PACKAGE_NAME)) + "<br>" + tr("Use up and down arrows to navigate history, and <b>Ctrl-L</b> to clear screen.") + "<br>" + - tr("Type <b>help</b> for an overview of available commands.")), true); + tr("Type <b>help</b> for an overview of available commands.")) + + "<br><span class=\"secwarning\">" + + tr("WARNING: Scammers have been active, telling users to type commands here, stealing their wallet contents. Do not use this console without fully understanding the ramification of a command.") + + "</span>", + true); } void RPCConsole::keyPressEvent(QKeyEvent *event) @@ -724,12 +803,30 @@ void RPCConsole::setMempoolSize(long numberOfTxs, size_t dynUsage) void RPCConsole::on_lineEdit_returnPressed() { QString cmd = ui->lineEdit->text(); - ui->lineEdit->clear(); if(!cmd.isEmpty()) { + std::string strFilteredCmd; + try { + std::string dummy; + if (!RPCParseCommandLine(dummy, cmd.toStdString(), false, &strFilteredCmd)) { + // Failed to parse command, so we cannot even filter it for the history + throw std::runtime_error("Invalid command line"); + } + } catch (const std::exception& e) { + QMessageBox::critical(this, "Error", QString("Error: ") + QString::fromStdString(e.what())); + return; + } + + ui->lineEdit->clear(); + + cmdBeforeBrowsing = QString(); + message(CMD_REQUEST, cmd); Q_EMIT cmdRequest(cmd); + + cmd = QString::fromStdString(strFilteredCmd); + // Remove command, if already in history history.removeOne(cmd); // Append command to history @@ -739,6 +836,7 @@ void RPCConsole::on_lineEdit_returnPressed() history.removeFirst(); // Set pointer to end of history historyPtr = history.size(); + // Scroll console view to end scrollToEnd(); } @@ -746,6 +844,11 @@ void RPCConsole::on_lineEdit_returnPressed() void RPCConsole::browseHistory(int offset) { + // store current text when start browsing through the history + if (historyPtr == history.size()) { + cmdBeforeBrowsing = ui->lineEdit->text(); + } + historyPtr += offset; if(historyPtr < 0) historyPtr = 0; @@ -754,14 +857,16 @@ void RPCConsole::browseHistory(int offset) QString cmd; if(historyPtr < history.size()) cmd = history.at(historyPtr); + else if (!cmdBeforeBrowsing.isNull()) { + cmd = cmdBeforeBrowsing; + } ui->lineEdit->setText(cmd); } void RPCConsole::startExecutor() { - QThread *thread = new QThread; RPCExecutor *executor = new RPCExecutor(); - executor->moveToThread(thread); + executor->moveToThread(&thread); // Replies from executor object must go to this object connect(executor, SIGNAL(reply(int,QString)), this, SLOT(message(int,QString))); @@ -769,16 +874,14 @@ void RPCConsole::startExecutor() connect(this, SIGNAL(cmdRequest(QString)), executor, SLOT(request(QString))); // On stopExecutor signal - // - queue executor for deletion (in execution thread) // - quit the Qt event loop in the execution thread - connect(this, SIGNAL(stopExecutor()), executor, SLOT(deleteLater())); - connect(this, SIGNAL(stopExecutor()), thread, SLOT(quit())); - // Queue the thread for deletion (in this thread) when it is finished - connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); + connect(this, SIGNAL(stopExecutor()), &thread, SLOT(quit())); + // - queue executor for deletion (in execution thread) + connect(&thread, SIGNAL(finished()), executor, SLOT(deleteLater()), Qt::DirectConnection); // Default implementation of QThread::run() simply spins up an event loop in the thread, // which is what we want. - thread->start(); + thread.start(); } void RPCConsole::on_tabWidget_currentChanged(int index) @@ -1004,11 +1107,11 @@ void RPCConsole::disconnectSelectedNode() return; // Get selected peer addresses - QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->peerWidget, 0); + QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId); for(int i = 0; i < nodes.count(); i++) { // Get currently selected peer address - NodeId id = nodes.at(i).data(PeerTableModel::NetNodeId).toInt(); + NodeId id = nodes.at(i).data().toInt(); // Find the node, disconnect it and clear the selected node if(g_connman->DisconnectNode(id)) clearSelectedNode(); @@ -1021,11 +1124,11 @@ void RPCConsole::banSelectedNode(int bantime) return; // Get selected peer addresses - QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->peerWidget, 0); + QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->peerWidget, PeerTableModel::NetNodeId); for(int i = 0; i < nodes.count(); i++) { // Get currently selected peer address - NodeId id = nodes.at(i).data(PeerTableModel::NetNodeId).toInt(); + NodeId id = nodes.at(i).data().toInt(); // Get currently selected peer address int detailNodeRow = clientModel->getPeerTableModel()->getRowByNodeId(id); @@ -1048,11 +1151,11 @@ void RPCConsole::unbanSelectedNode() return; // Get selected ban addresses - QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->banlistWidget, 0); + QList<QModelIndex> nodes = GUIUtil::getEntryData(ui->banlistWidget, BanTableModel::Address); for(int i = 0; i < nodes.count(); i++) { // Get currently selected ban address - QString strNode = nodes.at(i).data(BanTableModel::Address).toString(); + QString strNode = nodes.at(i).data().toString(); CSubNet possibleSubnet; LookupSubNet(strNode.toStdString().c_str(), possibleSubnet); @@ -1086,8 +1189,3 @@ void RPCConsole::setTabFocus(enum TabTypes tabType) { ui->tabWidget->setCurrentIndex(tabType); } - -void RPCConsole::on_toggleNetworkActiveButton_clicked() -{ - clientModel->setNetworkActive(!clientModel->getNetworkActive()); -} |