diff options
Diffstat (limited to 'src/qt/paymentserver.cpp')
-rw-r--r-- | src/qt/paymentserver.cpp | 559 |
1 files changed, 7 insertions, 552 deletions
diff --git a/src/qt/paymentserver.cpp b/src/qt/paymentserver.cpp index 00d83d23dd..6ad219ca2d 100644 --- a/src/qt/paymentserver.cpp +++ b/src/qt/paymentserver.cpp @@ -23,8 +23,6 @@ #include <cstdlib> #include <memory> -#include <openssl/x509_vfy.h> - #include <QApplication> #include <QByteArray> #include <QDataStream> @@ -36,28 +34,11 @@ #include <QList> #include <QLocalServer> #include <QLocalSocket> -#include <QNetworkAccessManager> -#include <QNetworkProxy> -#include <QNetworkReply> -#include <QNetworkRequest> -#include <QSslCertificate> -#include <QSslConfiguration> -#include <QSslError> #include <QStringList> -#include <QTextDocument> #include <QUrlQuery> const int BITCOIN_IPC_CONNECT_TIMEOUT = 1000; // milliseconds const QString BITCOIN_IPC_PREFIX("bitcoin:"); -#ifdef ENABLE_BIP70 -// BIP70 payment protocol messages -const char* BIP70_MESSAGE_PAYMENTACK = "PaymentACK"; -const char* BIP70_MESSAGE_PAYMENTREQUEST = "PaymentRequest"; -// BIP71 payment protocol media types -const char* BIP71_MIMETYPE_PAYMENT = "application/bitcoin-payment"; -const char* BIP71_MIMETYPE_PAYMENTACK = "application/bitcoin-paymentack"; -const char* BIP71_MIMETYPE_PAYMENTREQUEST = "application/bitcoin-paymentrequest"; -#endif // // Create a name that is unique for: @@ -82,7 +63,7 @@ static QString ipcServerName() // the main GUI window is up and ready to ask the user // to send payment. -static QList<QString> savedPaymentRequests; +static QSet<QString> savedPaymentRequests; // // Sending to the server is done synchronously, at startup. @@ -107,7 +88,8 @@ void PaymentServer::ipcParseCommandLine(interfaces::Node& node, int argc, char* // will start a mainnet instance and throw a "wrong network" error. if (arg.startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive)) // bitcoin: URI { - savedPaymentRequests.append(arg); + if (savedPaymentRequests.contains(arg)) continue; + savedPaymentRequests.insert(arg); SendCoinsRecipient r; if (GUIUtil::parseBitcoinURI(arg, &r) && !r.address.isEmpty()) @@ -124,31 +106,6 @@ void PaymentServer::ipcParseCommandLine(interfaces::Node& node, int argc, char* } } } -#ifdef ENABLE_BIP70 - else if (QFile::exists(arg)) // Filename - { - savedPaymentRequests.append(arg); - - PaymentRequestPlus request; - if (readPaymentRequestFromFile(arg, request)) - { - if (request.getDetails().network() == "main") - { - node.selectParams(CBaseChainParams::MAIN); - } - else if (request.getDetails().network() == "test") - { - node.selectParams(CBaseChainParams::TESTNET); - } - } - } - else - { - // Printing to debug.log is about the best we can do here, the - // GUI hasn't started yet so we can't pop up a message box. - qWarning() << "PaymentServer::ipcSendCommandLine: Payment request file does not exist: " << arg; - } -#endif } } @@ -196,16 +153,7 @@ PaymentServer::PaymentServer(QObject* parent, bool startLocalServer) : saveURIs(true), uriServer(nullptr), optionsModel(nullptr) -#ifdef ENABLE_BIP70 - ,netManager(nullptr) -#endif { -#ifdef ENABLE_BIP70 - // Verify that the version of the library that we linked against is - // compatible with the version of the headers we compiled against. - GOOGLE_PROTOBUF_VERIFY_VERSION; -#endif - // Install global event filter to catch QFileOpenEvents // on Mac: sent when you click bitcoin: links // other OSes: helpful when dealing with payment request files @@ -228,24 +176,16 @@ PaymentServer::PaymentServer(QObject* parent, bool startLocalServer) : } else { connect(uriServer, &QLocalServer::newConnection, this, &PaymentServer::handleURIConnection); -#ifdef ENABLE_BIP70 - connect(this, &PaymentServer::receivedPaymentACK, this, &PaymentServer::handlePaymentACK); -#endif } } } PaymentServer::~PaymentServer() { -#ifdef ENABLE_BIP70 - google::protobuf::ShutdownProtobufLibrary(); -#endif } // -// OSX-specific way of handling bitcoin: URIs and PaymentRequest mime types. -// Also used by paymentservertests.cpp and when opening a payment request file -// via "Open URI..." menu entry. +// OSX-specific way of handling bitcoin: URIs // bool PaymentServer::eventFilter(QObject *object, QEvent *event) { @@ -264,10 +204,6 @@ bool PaymentServer::eventFilter(QObject *object, QEvent *event) void PaymentServer::uiReady() { -#ifdef ENABLE_BIP70 - initNetManager(); -#endif - saveURIs = false; for (const QString& s : savedPaymentRequests) { @@ -280,7 +216,7 @@ void PaymentServer::handleURIOrFile(const QString& s) { if (saveURIs) { - savedPaymentRequests.append(s); + savedPaymentRequests.insert(s); return; } @@ -292,48 +228,19 @@ void PaymentServer::handleURIOrFile(const QString& s) else if (s.startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive)) // bitcoin: URI { QUrlQuery uri((QUrl(s))); -#ifdef ENABLE_BIP70 - if (uri.hasQueryItem("r")) // payment request URI - { - Q_EMIT message(tr("URI handling"), - tr("You are using a BIP70 URL which will be unsupported in the future."), - CClientUIInterface::ICON_WARNING); - QByteArray temp; - temp.append(uri.queryItemValue("r")); - QString decoded = QUrl::fromPercentEncoding(temp); - QUrl fetchUrl(decoded, QUrl::StrictMode); - - if (fetchUrl.isValid()) - { - qDebug() << "PaymentServer::handleURIOrFile: fetchRequest(" << fetchUrl << ")"; - fetchRequest(fetchUrl); - } - else - { - qWarning() << "PaymentServer::handleURIOrFile: Invalid URL: " << fetchUrl; - Q_EMIT message(tr("URI handling"), - tr("Payment request fetch URL is invalid: %1").arg(fetchUrl.toString()), - CClientUIInterface::ICON_WARNING); - } - return; - } - else -#endif // normal URI { SendCoinsRecipient recipient; if (GUIUtil::parseBitcoinURI(s, &recipient)) { if (!IsValidDestinationString(recipient.address.toStdString())) { -#ifndef ENABLE_BIP70 if (uri.hasQueryItem("r")) { // payment request Q_EMIT message(tr("URI handling"), - tr("Cannot process payment request because BIP70 support was not compiled in.")+ + tr("Cannot process payment request because BIP70 is not supported.")+ tr("Due to widespread security flaws in BIP70 it's strongly recommended that any merchant instructions to switch wallets be ignored.")+ tr("If you are receiving this error you should request the merchant provide a BIP21 compatible URI."), CClientUIInterface::ICON_WARNING); } -#endif Q_EMIT message(tr("URI handling"), tr("Invalid payment address %1").arg(recipient.address), CClientUIInterface::MSG_ERROR); } @@ -351,26 +258,11 @@ void PaymentServer::handleURIOrFile(const QString& s) if (QFile::exists(s)) // payment request file { -#ifdef ENABLE_BIP70 - PaymentRequestPlus request; - SendCoinsRecipient recipient; - if (!readPaymentRequestFromFile(s, request)) - { - Q_EMIT message(tr("Payment request file handling"), - tr("Payment request file cannot be read! This can be caused by an invalid payment request file."), - CClientUIInterface::ICON_WARNING); - } - else if (processPaymentRequest(request, recipient)) - Q_EMIT receivedPaymentRequest(recipient); - - return; -#else Q_EMIT message(tr("Payment request file handling"), - tr("Cannot process payment request because BIP70 support was not compiled in.")+ + tr("Cannot process payment request because BIP70 is not supported.")+ tr("Due to widespread security flaws in BIP70 it's strongly recommended that any merchant instructions to switch wallets be ignored.")+ tr("If you are receiving this error you should request the merchant provide a BIP21 compatible URI."), CClientUIInterface::ICON_WARNING); -#endif } } @@ -398,440 +290,3 @@ void PaymentServer::setOptionsModel(OptionsModel *_optionsModel) { this->optionsModel = _optionsModel; } - -#ifdef ENABLE_BIP70 -struct X509StoreDeleter { - void operator()(X509_STORE* b) { - X509_STORE_free(b); - } -}; - -struct X509Deleter { - void operator()(X509* b) { X509_free(b); } -}; - -namespace // Anon namespace -{ - std::unique_ptr<X509_STORE, X509StoreDeleter> certStore; -} - -static void ReportInvalidCertificate(const QSslCertificate& cert) -{ - qDebug() << QString("%1: Payment server found an invalid certificate: ").arg(__func__) << cert.serialNumber() << cert.subjectInfo(QSslCertificate::CommonName) << cert.subjectInfo(QSslCertificate::DistinguishedNameQualifier) << cert.subjectInfo(QSslCertificate::OrganizationalUnitName); -} - -// -// Load OpenSSL's list of root certificate authorities -// -void PaymentServer::LoadRootCAs(X509_STORE* _store) -{ - // Unit tests mostly use this, to pass in fake root CAs: - if (_store) - { - certStore.reset(_store); - return; - } - - // Normal execution, use either -rootcertificates or system certs: - certStore.reset(X509_STORE_new()); - - // Note: use "-system-" default here so that users can pass -rootcertificates="" - // and get 'I don't like X.509 certificates, don't trust anybody' behavior: - QString certFile = QString::fromStdString(gArgs.GetArg("-rootcertificates", "-system-")); - - // Empty store - if (certFile.isEmpty()) { - qDebug() << QString("PaymentServer::%1: Payment request authentication via X.509 certificates disabled.").arg(__func__); - return; - } - - QList<QSslCertificate> certList; - - if (certFile != "-system-") { - qDebug() << QString("PaymentServer::%1: Using \"%2\" as trusted root certificate.").arg(__func__).arg(certFile); - - certList = QSslCertificate::fromPath(certFile); - // Use those certificates when fetching payment requests, too: - QSslConfiguration::defaultConfiguration().setCaCertificates(certList); - } else - certList = QSslConfiguration::systemCaCertificates(); - - int nRootCerts = 0; - const QDateTime currentTime = QDateTime::currentDateTime(); - - for (const QSslCertificate& cert : certList) { - // Don't log NULL certificates - if (cert.isNull()) - continue; - - // Not yet active/valid, or expired certificate - if (currentTime < cert.effectiveDate() || currentTime > cert.expiryDate()) { - ReportInvalidCertificate(cert); - continue; - } - - // Blacklisted certificate - if (cert.isBlacklisted()) { - ReportInvalidCertificate(cert); - continue; - } - - QByteArray certData = cert.toDer(); - const unsigned char *data = (const unsigned char *)certData.data(); - - std::unique_ptr<X509, X509Deleter> x509(d2i_X509(0, &data, certData.size())); - if (x509 && X509_STORE_add_cert(certStore.get(), x509.get())) - { - // Note: X509_STORE increases the reference count to the X509 object, - // we still have to release our reference to it. - ++nRootCerts; - } - else - { - ReportInvalidCertificate(cert); - continue; - } - } - qInfo() << "PaymentServer::LoadRootCAs: Loaded " << nRootCerts << " root certificates"; - - // Project for another day: - // Fetch certificate revocation lists, and add them to certStore. - // Issues to consider: - // performance (start a thread to fetch in background?) - // privacy (fetch through tor/proxy so IP address isn't revealed) - // would it be easier to just use a compiled-in blacklist? - // or use Qt's blacklist? - // "certificate stapling" with server-side caching is more efficient -} - -void PaymentServer::initNetManager() -{ - if (!optionsModel) - return; - delete netManager; - - // netManager is used to fetch paymentrequests given in bitcoin: URIs - netManager = new QNetworkAccessManager(this); - - QNetworkProxy proxy; - - // Query active SOCKS5 proxy - if (optionsModel->getProxySettings(proxy)) { - netManager->setProxy(proxy); - - qDebug() << "PaymentServer::initNetManager: Using SOCKS5 proxy" << proxy.hostName() << ":" << proxy.port(); - } - else - qDebug() << "PaymentServer::initNetManager: No active proxy server found."; - - connect(netManager, &QNetworkAccessManager::finished, this, &PaymentServer::netRequestFinished); - connect(netManager, &QNetworkAccessManager::sslErrors, this, &PaymentServer::reportSslErrors); -} - -// -// Warning: readPaymentRequestFromFile() is used in ipcSendCommandLine() -// so don't use "Q_EMIT message()", but "QMessageBox::"! -// -bool PaymentServer::readPaymentRequestFromFile(const QString& filename, PaymentRequestPlus& request) -{ - QFile f(filename); - if (!f.open(QIODevice::ReadOnly)) { - qWarning() << QString("PaymentServer::%1: Failed to open %2").arg(__func__).arg(filename); - return false; - } - - // BIP70 DoS protection - if (!verifySize(f.size())) { - return false; - } - - QByteArray data = f.readAll(); - - return request.parse(data); -} - -bool PaymentServer::processPaymentRequest(const PaymentRequestPlus& request, SendCoinsRecipient& recipient) -{ - if (!optionsModel) - return false; - - if (request.IsInitialized()) { - // Payment request network matches client network? - if (!verifyNetwork(optionsModel->node(), request.getDetails())) { - Q_EMIT message(tr("Payment request rejected"), tr("Payment request network doesn't match client network."), - CClientUIInterface::MSG_ERROR); - - return false; - } - - // Make sure any payment requests involved are still valid. - // This is re-checked just before sending coins in WalletModel::sendCoins(). - if (verifyExpired(request.getDetails())) { - Q_EMIT message(tr("Payment request rejected"), tr("Payment request expired."), - CClientUIInterface::MSG_ERROR); - - return false; - } - } else { - Q_EMIT message(tr("Payment request error"), tr("Payment request is not initialized."), - CClientUIInterface::MSG_ERROR); - - return false; - } - - recipient.paymentRequest = request; - recipient.message = GUIUtil::HtmlEscape(request.getDetails().memo()); - - request.getMerchant(certStore.get(), recipient.authenticatedMerchant); - - QList<std::pair<CScript, CAmount> > sendingTos = request.getPayTo(); - QStringList addresses; - - for (const std::pair<CScript, CAmount>& sendingTo : sendingTos) { - // Extract and check destination addresses - CTxDestination dest; - if (ExtractDestination(sendingTo.first, dest)) { - // Append destination address - addresses.append(QString::fromStdString(EncodeDestination(dest))); - } - else if (!recipient.authenticatedMerchant.isEmpty()) { - // Unauthenticated payment requests to custom bitcoin addresses are not supported - // (there is no good way to tell the user where they are paying in a way they'd - // have a chance of understanding). - Q_EMIT message(tr("Payment request rejected"), - tr("Unverified payment requests to custom payment scripts are unsupported."), - CClientUIInterface::MSG_ERROR); - return false; - } - - // Bitcoin amounts are stored as (optional) uint64 in the protobuf messages (see paymentrequest.proto), - // but CAmount is defined as int64_t. Because of that we need to verify that amounts are in a valid range - // and no overflow has happened. - if (!verifyAmount(sendingTo.second)) { - Q_EMIT message(tr("Payment request rejected"), tr("Invalid payment request."), CClientUIInterface::MSG_ERROR); - return false; - } - - // Extract and check amounts - CTxOut txOut(sendingTo.second, sendingTo.first); - if (IsDust(txOut, optionsModel->node().getDustRelayFee())) { - Q_EMIT message(tr("Payment request error"), tr("Requested payment amount of %1 is too small (considered dust).") - .arg(BitcoinUnits::formatWithUnit(optionsModel->getDisplayUnit(), sendingTo.second)), - CClientUIInterface::MSG_ERROR); - - return false; - } - - recipient.amount += sendingTo.second; - // Also verify that the final amount is still in a valid range after adding additional amounts. - if (!verifyAmount(recipient.amount)) { - Q_EMIT message(tr("Payment request rejected"), tr("Invalid payment request."), CClientUIInterface::MSG_ERROR); - return false; - } - } - // Store addresses and format them to fit nicely into the GUI - recipient.address = addresses.join("<br />"); - - if (!recipient.authenticatedMerchant.isEmpty()) { - qDebug() << "PaymentServer::processPaymentRequest: Secure payment request from " << recipient.authenticatedMerchant; - } - else { - qDebug() << "PaymentServer::processPaymentRequest: Insecure payment request to " << addresses.join(", "); - } - - return true; -} - -void PaymentServer::fetchRequest(const QUrl& url) -{ - QNetworkRequest netRequest; - netRequest.setAttribute(QNetworkRequest::User, BIP70_MESSAGE_PAYMENTREQUEST); - netRequest.setUrl(url); - netRequest.setRawHeader("User-Agent", CLIENT_NAME.c_str()); - netRequest.setRawHeader("Accept", BIP71_MIMETYPE_PAYMENTREQUEST); - netManager->get(netRequest); -} - -void PaymentServer::fetchPaymentACK(WalletModel* walletModel, const SendCoinsRecipient& recipient, QByteArray transaction) -{ - const payments::PaymentDetails& details = recipient.paymentRequest.getDetails(); - if (!details.has_payment_url()) - return; - - QNetworkRequest netRequest; - netRequest.setAttribute(QNetworkRequest::User, BIP70_MESSAGE_PAYMENTACK); - netRequest.setUrl(QString::fromStdString(details.payment_url())); - netRequest.setHeader(QNetworkRequest::ContentTypeHeader, BIP71_MIMETYPE_PAYMENT); - netRequest.setRawHeader("User-Agent", CLIENT_NAME.c_str()); - netRequest.setRawHeader("Accept", BIP71_MIMETYPE_PAYMENTACK); - - payments::Payment payment; - payment.set_merchant_data(details.merchant_data()); - payment.add_transactions(transaction.data(), transaction.size()); - - // Create a new refund address, or re-use: - CTxDestination dest; - const OutputType change_type = walletModel->wallet().getDefaultChangeType() != OutputType::CHANGE_AUTO ? walletModel->wallet().getDefaultChangeType() : walletModel->wallet().getDefaultAddressType(); - if (walletModel->wallet().getNewDestination(change_type, "", dest)) { - // BIP70 requests encode the scriptPubKey directly, so we are not restricted to address - // types supported by the receiver. As a result, we choose the address format we also - // use for change. Despite an actual payment and not change, this is a close match: - // it's the output type we use subject to privacy issues, but not restricted by what - // other software supports. - std::string label = tr("Refund from %1").arg(recipient.authenticatedMerchant).toStdString(); - walletModel->wallet().setAddressBook(dest, label, "refund"); - - CScript s = GetScriptForDestination(dest); - payments::Output* refund_to = payment.add_refund_to(); - refund_to->set_script(&s[0], s.size()); - } else { - // This should never happen, because sending coins should have - // just unlocked the wallet and refilled the keypool. - qWarning() << "PaymentServer::fetchPaymentACK: Error getting refund key, refund_to not set"; - } - - int length = payment.ByteSize(); - netRequest.setHeader(QNetworkRequest::ContentLengthHeader, length); - QByteArray serData(length, '\0'); - if (payment.SerializeToArray(serData.data(), length)) { - netManager->post(netRequest, serData); - } - else { - // This should never happen, either. - qWarning() << "PaymentServer::fetchPaymentACK: Error serializing payment message"; - } -} - -void PaymentServer::netRequestFinished(QNetworkReply* reply) -{ - reply->deleteLater(); - - // BIP70 DoS protection - if (!verifySize(reply->size())) { - Q_EMIT message(tr("Payment request rejected"), - tr("Payment request %1 is too large (%2 bytes, allowed %3 bytes).") - .arg(reply->request().url().toString()) - .arg(reply->size()) - .arg(BIP70_MAX_PAYMENTREQUEST_SIZE), - CClientUIInterface::MSG_ERROR); - return; - } - - if (reply->error() != QNetworkReply::NoError) { - QString msg = tr("Error communicating with %1: %2") - .arg(reply->request().url().toString()) - .arg(reply->errorString()); - - qWarning() << "PaymentServer::netRequestFinished: " << msg; - Q_EMIT message(tr("Payment request error"), msg, CClientUIInterface::MSG_ERROR); - return; - } - - QByteArray data = reply->readAll(); - - QString requestType = reply->request().attribute(QNetworkRequest::User).toString(); - if (requestType == BIP70_MESSAGE_PAYMENTREQUEST) - { - PaymentRequestPlus request; - SendCoinsRecipient recipient; - if (!request.parse(data)) - { - qWarning() << "PaymentServer::netRequestFinished: Error parsing payment request"; - Q_EMIT message(tr("Payment request error"), - tr("Payment request cannot be parsed!"), - CClientUIInterface::MSG_ERROR); - } - else if (processPaymentRequest(request, recipient)) - Q_EMIT receivedPaymentRequest(recipient); - - return; - } - else if (requestType == BIP70_MESSAGE_PAYMENTACK) - { - payments::PaymentACK paymentACK; - if (!paymentACK.ParseFromArray(data.data(), data.size())) - { - QString msg = tr("Bad response from server %1") - .arg(reply->request().url().toString()); - - qWarning() << "PaymentServer::netRequestFinished: " << msg; - Q_EMIT message(tr("Payment request error"), msg, CClientUIInterface::MSG_ERROR); - } - else - { - Q_EMIT receivedPaymentACK(GUIUtil::HtmlEscape(paymentACK.memo())); - } - } -} - -void PaymentServer::reportSslErrors(QNetworkReply* reply, const QList<QSslError> &errs) -{ - Q_UNUSED(reply); - - QString errString; - for (const QSslError& err : errs) { - qWarning() << "PaymentServer::reportSslErrors: " << err; - errString += err.errorString() + "\n"; - } - Q_EMIT message(tr("Network request error"), errString, CClientUIInterface::MSG_ERROR); -} - -void PaymentServer::handlePaymentACK(const QString& paymentACKMsg) -{ - // currently we don't further process or store the paymentACK message - Q_EMIT message(tr("Payment acknowledged"), paymentACKMsg, CClientUIInterface::ICON_INFORMATION | CClientUIInterface::MODAL); -} - -bool PaymentServer::verifyNetwork(interfaces::Node& node, const payments::PaymentDetails& requestDetails) -{ - bool fVerified = requestDetails.network() == node.getNetwork(); - if (!fVerified) { - qWarning() << QString("PaymentServer::%1: Payment request network \"%2\" doesn't match client network \"%3\".") - .arg(__func__) - .arg(QString::fromStdString(requestDetails.network())) - .arg(QString::fromStdString(node.getNetwork())); - } - return fVerified; -} - -bool PaymentServer::verifyExpired(const payments::PaymentDetails& requestDetails) -{ - bool fVerified = (requestDetails.has_expires() && (int64_t)requestDetails.expires() < GetTime()); - if (fVerified) { - const QString requestExpires = QString::fromStdString(FormatISO8601DateTime((int64_t)requestDetails.expires())); - qWarning() << QString("PaymentServer::%1: Payment request expired \"%2\".") - .arg(__func__) - .arg(requestExpires); - } - return fVerified; -} - -bool PaymentServer::verifySize(qint64 requestSize) -{ - bool fVerified = (requestSize <= BIP70_MAX_PAYMENTREQUEST_SIZE); - if (!fVerified) { - qWarning() << QString("PaymentServer::%1: Payment request too large (%2 bytes, allowed %3 bytes).") - .arg(__func__) - .arg(requestSize) - .arg(BIP70_MAX_PAYMENTREQUEST_SIZE); - } - return fVerified; -} - -bool PaymentServer::verifyAmount(const CAmount& requestAmount) -{ - bool fVerified = MoneyRange(requestAmount); - if (!fVerified) { - qWarning() << QString("PaymentServer::%1: Payment request amount out of allowed range (%2, allowed 0 - %3).") - .arg(__func__) - .arg(requestAmount) - .arg(MAX_MONEY); - } - return fVerified; -} - -X509_STORE* PaymentServer::getCertStore() -{ - return certStore.get(); -} -#endif |