diff options
Diffstat (limited to 'src/qt/paymentserver.cpp')
-rw-r--r-- | src/qt/paymentserver.cpp | 558 |
1 files changed, 514 insertions, 44 deletions
diff --git a/src/qt/paymentserver.cpp b/src/qt/paymentserver.cpp index 0b0bce55bb..af75d6b4e5 100644 --- a/src/qt/paymentserver.cpp +++ b/src/qt/paymentserver.cpp @@ -1,31 +1,64 @@ -// Copyright (c) 2009-2012 The Bitcoin developers +// Copyright (c) 2009-2013 The Bitcoin developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include <QApplication> - -#include "paymentserver.h" - -#include "guiconstants.h" -#include "ui_interface.h" -#include "util.h" - #include <QByteArray> #include <QDataStream> +#include <QDateTime> #include <QDebug> +#include <QFile> #include <QFileOpenEvent> #include <QHash> +#include <QList> #include <QLocalServer> #include <QLocalSocket> #include <QStringList> +#include <QTextDocument> +#include <QNetworkAccessManager> +#include <QNetworkProxy> +#include <QNetworkReply> +#include <QNetworkRequest> +#include <QSslCertificate> +#include <QSslError> +#include <QSslSocket> #if QT_VERSION < 0x050000 #include <QUrl> +#else +#include <QUrlQuery> #endif -using namespace boost; +#include <cstdlib> + +#include <openssl/x509.h> +#include <openssl/x509_vfy.h> + +#include "base58.h" +#include "bitcoinunits.h" +#include "guiconstants.h" +#include "guiutil.h" +#include "optionsmodel.h" +#include "paymentserver.h" +#include "ui_interface.h" +#include "util.h" +#include "wallet.h" +#include "walletmodel.h" const int BITCOIN_IPC_CONNECT_TIMEOUT = 1000; // milliseconds const QString BITCOIN_IPC_PREFIX("bitcoin:"); +const char* BITCOIN_REQUEST_MIMETYPE = "application/bitcoin-paymentrequest"; +const char* BITCOIN_PAYMENTACK_MIMETYPE = "application/bitcoin-paymentack"; +const char* BITCOIN_PAYMENTACK_CONTENTTYPE = "application/bitcoin-payment"; + +X509_STORE* PaymentServer::certStore = NULL; +void PaymentServer::freeCertStore() +{ + if (PaymentServer::certStore != NULL) + { + X509_STORE_free(PaymentServer::certStore); + PaymentServer::certStore = NULL; + } +} // // Create a name that is unique for: @@ -39,18 +72,106 @@ static QString ipcServerName() // Append a simple hash of the datadir // Note that GetDataDir(true) returns a different path // for -testnet versus main net - QString ddir(GetDataDir(true).string().c_str()); + QString ddir(QString::fromStdString(GetDataDir(true).string())); name.append(QString::number(qHash(ddir))); return name; } // -// This stores payment requests received before +// We store payment URIs and requests received before // the main GUI window is up and ready to ask the user // to send payment. + +static QList<QString> savedPaymentRequests; + +static void ReportInvalidCertificate(const QSslCertificate& cert) +{ + if (fDebug) { + qDebug() << "ReportInvalidCertificate : Payment server found an invalid certificate: " << cert.subjectInfo(QSslCertificate::CommonName); + } +} + // -static QStringList savedPaymentRequests; +// Load OpenSSL's list of root certificate authorities +// +void PaymentServer::LoadRootCAs(X509_STORE* _store) +{ + if (PaymentServer::certStore == NULL) + atexit(PaymentServer::freeCertStore); + else + freeCertStore(); + + // Unit tests mostly use this, to pass in fake root CAs: + if (_store) + { + PaymentServer::certStore = _store; + return; + } + + // Normal execution, use either -rootcertificates or system certs: + PaymentServer::certStore = 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(GetArg("-rootcertificates", "-system-")); + + if (certFile.isEmpty()) + return; // Empty store + + QList<QSslCertificate> certList; + + if (certFile != "-system-") + { + certList = QSslCertificate::fromPath(certFile); + // Use those certificates when fetching payment requests, too: + QSslSocket::setDefaultCaCertificates(certList); + } + else + certList = QSslSocket::systemCaCertificates (); + + int nRootCerts = 0; + const QDateTime currentTime = QDateTime::currentDateTime(); + foreach (const QSslCertificate& cert, certList) + { + if (currentTime < cert.effectiveDate() || currentTime > cert.expiryDate()) { + ReportInvalidCertificate(cert); + continue; + } +#if QT_VERSION >= 0x050000 + if (cert.isBlacklisted()) { + ReportInvalidCertificate(cert); + continue; + } +#endif + QByteArray certData = cert.toDer(); + const unsigned char *data = (const unsigned char *)certData.data(); + + X509* x509 = d2i_X509(0, &data, certData.size()); + if (x509 && X509_STORE_add_cert(PaymentServer::certStore, x509)) + { + // Note: X509_STORE_free will free the X509* objects when + // the PaymentServer is destroyed + ++nRootCerts; + } + else + { + ReportInvalidCertificate(cert); + continue; + } + } + if (fDebug) + qDebug() << "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 +} // // Sending to the server is done synchronously, at startup. @@ -58,19 +179,54 @@ static QStringList savedPaymentRequests; // and the items in savedPaymentRequest will be handled // when uiReady() is called. // -bool PaymentServer::ipcSendCommandLine() +bool PaymentServer::ipcSendCommandLine(int argc, char* argv[]) { bool fResult = false; - const QStringList& args = qApp->arguments(); - for (int i = 1; i < args.size(); i++) + for (int i = 1; i < argc; i++) { - if (!args[i].startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive)) + QString arg(argv[i]); + if (arg.startsWith("-")) continue; - savedPaymentRequests.append(args[i]); + + if (arg.startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive)) // bitcoin: URI + { + savedPaymentRequests.append(arg); + + SendCoinsRecipient r; + if (GUIUtil::parseBitcoinURI(arg, &r)) + { + CBitcoinAddress address(r.address.toStdString()); + + SelectParams(CChainParams::MAIN); + if (!address.IsValid()) + { + SelectParams(CChainParams::TESTNET); + } + } + } + else if (QFile::exists(arg)) // Filename + { + savedPaymentRequests.append(arg); + + PaymentRequestPlus request; + if (readPaymentRequest(arg, request)) + { + if (request.getDetails().network() == "main") + SelectParams(CChainParams::MAIN); + else + SelectParams(CChainParams::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. + qDebug() << "PaymentServer::ipcSendCommandLine : Payment request file does not exist: " << arg; + } } - foreach (const QString& arg, savedPaymentRequests) + foreach (const QString& r, savedPaymentRequests) { QLocalSocket* socket = new QLocalSocket(); socket->connectToServer(ipcServerName(), QIODevice::WriteOnly); @@ -80,7 +236,7 @@ bool PaymentServer::ipcSendCommandLine() QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setVersion(QDataStream::Qt_4_0); - out << arg; + out << r; out.device()->seek(0); socket->write(block); socket->flush(); @@ -90,53 +246,161 @@ bool PaymentServer::ipcSendCommandLine() delete socket; fResult = true; } + return fResult; } -PaymentServer::PaymentServer(QApplication* parent) : QObject(parent), saveURIs(true) +PaymentServer::PaymentServer(QObject* parent, bool startLocalServer) : QObject(parent), saveURIs(true) { - // Install global event filter to catch QFileOpenEvents on the mac (sent when you click bitcoin: links) - parent->installEventFilter(this); + // 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; + + // Install global event filter to catch QFileOpenEvents + // on Mac: sent when you click bitcoin: links + // other OSes: helpful when dealing with payment-request files (in the future) + if (parent) + parent->installEventFilter(this); QString name = ipcServerName(); // Clean up old socket leftover from a crash: QLocalServer::removeServer(name); - uriServer = new QLocalServer(this); + if (startLocalServer) + { + uriServer = new QLocalServer(this); - if (!uriServer->listen(name)) - qDebug() << tr("Cannot start bitcoin: click-to-pay handler"); - else - connect(uriServer, SIGNAL(newConnection()), this, SLOT(handleURIConnection())); + if (!uriServer->listen(name)) + qDebug() << "PaymentServer::PaymentServer : Cannot start bitcoin: click-to-pay handler"; + else + connect(uriServer, SIGNAL(newConnection()), this, SLOT(handleURIConnection())); + } + + // netManager is null until uiReady() is called + netManager = NULL; +} + +PaymentServer::~PaymentServer() +{ + google::protobuf::ShutdownProtobufLibrary(); } -bool PaymentServer::eventFilter(QObject *object, QEvent *event) +// +// OSX-specific way of handling bitcoin: URIs and +// PaymentRequest mime types +// +bool PaymentServer::eventFilter(QObject *, QEvent *event) { - // clicking on bitcoin: URLs creates FileOpen events on the Mac: + // clicking on bitcoin: URIs creates FileOpen events on the Mac: if (event->type() == QEvent::FileOpen) { QFileOpenEvent* fileEvent = static_cast<QFileOpenEvent*>(event); - if (!fileEvent->url().isEmpty()) - { - if (saveURIs) // Before main window is ready: - savedPaymentRequests.append(fileEvent->url().toString()); - else - emit receivedURI(fileEvent->url().toString()); - return true; - } + if (!fileEvent->file().isEmpty()) + handleURIOrFile(fileEvent->file()); + else if (!fileEvent->url().isEmpty()) + handleURIOrFile(fileEvent->url().toString()); + + return true; } return false; } +void PaymentServer::initNetManager() +{ + if (!optionsModel) + return; + if (netManager != NULL) + delete netManager; + + // netManager is used to fetch paymentrequests given in bitcoin: URIs + netManager = new QNetworkAccessManager(this); + + // Use proxy settings from optionsModel: + QString proxyIP; + quint16 proxyPort; + if (optionsModel->getProxySettings(proxyIP, proxyPort)) + { + QNetworkProxy proxy; + proxy.setType(QNetworkProxy::Socks5Proxy); + proxy.setHostName(proxyIP); + proxy.setPort(proxyPort); + netManager->setProxy(proxy); + } + + connect(netManager, SIGNAL(finished(QNetworkReply*)), + this, SLOT(netRequestFinished(QNetworkReply*))); + connect(netManager, SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError> &)), + this, SLOT(reportSslErrors(QNetworkReply*, const QList<QSslError> &))); +} + void PaymentServer::uiReady() { + assert(netManager != NULL); // Must call initNetManager before uiReady() + saveURIs = false; foreach (const QString& s, savedPaymentRequests) - emit receivedURI(s); + { + handleURIOrFile(s); + } savedPaymentRequests.clear(); } +void PaymentServer::handleURIOrFile(const QString& s) +{ + if (saveURIs) + { + savedPaymentRequests.append(s); + return; + } + + if (s.startsWith(BITCOIN_IPC_PREFIX, Qt::CaseInsensitive)) // bitcoin: + { +#if QT_VERSION >= 0x050000 + QUrlQuery uri((QUrl(s))); +#else + QUrl uri(s); +#endif + if (uri.hasQueryItem("request")) + { + QByteArray temp; + temp.append(uri.queryItemValue("request")); + QString decoded = QUrl::fromPercentEncoding(temp); + QUrl fetchUrl(decoded, QUrl::StrictMode); + + if (fDebug) + qDebug() << "PaymentServer::handleURIOrFile : fetchRequest(" << fetchUrl << ")"; + + if (fetchUrl.isValid()) + fetchRequest(fetchUrl); + else + qDebug() << "PaymentServer::handleURIOrFile : Invalid URL: " << fetchUrl; + return; + } + + SendCoinsRecipient recipient; + if (GUIUtil::parseBitcoinURI(s, &recipient)) + emit receivedPaymentRequest(recipient); + else + emit message(tr("URI handling"), + tr("URI can not be parsed! This can be caused by an invalid Bitcoin address or malformed URI parameters."), + CClientUIInterface::ICON_WARNING); + return; + } + + if (QFile::exists(s)) + { + PaymentRequestPlus request; + QList<SendCoinsRecipient> recipients; + if (readPaymentRequest(s, request) && processPaymentRequest(request, recipients)) { + foreach (const SendCoinsRecipient& recipient, recipients){ + emit receivedPaymentRequest(recipient); + } + } + return; + } +} + void PaymentServer::handleURIConnection() { QLocalSocket *clientConnection = uriServer->nextPendingConnection(); @@ -152,11 +416,217 @@ void PaymentServer::handleURIConnection() if (clientConnection->bytesAvailable() < (int)sizeof(quint16)) { return; } - QString message; - in >> message; + QString msg; + in >> msg; - if (saveURIs) - savedPaymentRequests.append(message); - else - emit receivedURI(message); + handleURIOrFile(msg); +} + +bool PaymentServer::readPaymentRequest(const QString& filename, PaymentRequestPlus& request) +{ + QFile f(filename); + if (!f.open(QIODevice::ReadOnly)) + { + qDebug() << "PaymentServer::readPaymentRequest : Failed to open " << filename; + return false; + } + + if (f.size() > MAX_PAYMENT_REQUEST_SIZE) + { + qDebug() << "PaymentServer::readPaymentRequest : " << filename << " too large"; + return false; + } + + QByteArray data = f.readAll(); + + return request.parse(data); +} + +bool PaymentServer::processPaymentRequest(PaymentRequestPlus& request, QList<SendCoinsRecipient>& recipients) +{ + if (!optionsModel) + return false; + + QList<std::pair<CScript,qint64> > sendingTos = request.getPayTo(); + qint64 totalAmount = 0; + foreach(const PAIRTYPE(CScript, qint64)& sendingTo, sendingTos) { + CTxOut txOut(sendingTo.second, sendingTo.first); + if (txOut.IsDust(CTransaction::nMinRelayTxFee)) { + QString msg = QObject::tr("Requested payment amount (%1) too small") + .arg(BitcoinUnits::formatWithUnit(optionsModel->getDisplayUnit(), sendingTo.second)); + + qDebug() << "PaymentServer::processPaymentRequest : " << msg; + emit message(tr("Payment request error"), msg, CClientUIInterface::MSG_ERROR); + return false; + } + + totalAmount += sendingTo.second; + } + + recipients.append(SendCoinsRecipient()); + + if (request.getMerchant(PaymentServer::certStore, recipients[0].authenticatedMerchant)) { + recipients[0].paymentRequest = request; + recipients[0].amount = totalAmount; + if (fDebug) + qDebug() << "PaymentServer::processPaymentRequest : Payment request from " << recipients[0].authenticatedMerchant; + } + else { + recipients.clear(); + // Insecure payment requests may turn into more than one recipient if + // the merchant is requesting payment to more than one address. + for (int i = 0; i < sendingTos.size(); i++) { + std::pair<CScript, qint64>& sendingTo = sendingTos[i]; + recipients.append(SendCoinsRecipient()); + recipients[i].amount = sendingTo.second; + QString memo = QString::fromStdString(request.getDetails().memo()); + recipients[i].label = GUIUtil::HtmlEscape(memo); + CTxDestination dest; + if (ExtractDestination(sendingTo.first, dest)) { + if (i == 0) // Tie request to first pay-to, we don't want multiple ACKs + recipients[i].paymentRequest = request; + recipients[i].address = QString::fromStdString(CBitcoinAddress(dest).ToString()); + if (fDebug) + qDebug() << "PaymentServer::processPaymentRequest : Payment request, insecure " << recipients[i].address; + } + else { + // Insecure payments 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). + emit message(tr("Payment request error"), + tr("Insecure requests to custom payment scripts unsupported"), + CClientUIInterface::MSG_ERROR); + return false; + } + } + } + + return true; +} + +void PaymentServer::fetchRequest(const QUrl& url) +{ + QNetworkRequest netRequest; + netRequest.setAttribute(QNetworkRequest::User, "PaymentRequest"); + netRequest.setUrl(url); + netRequest.setRawHeader("User-Agent", CLIENT_NAME.c_str()); + netRequest.setRawHeader("Accept", BITCOIN_REQUEST_MIMETYPE); + netManager->get(netRequest); +} + +void PaymentServer::fetchPaymentACK(CWallet* wallet, SendCoinsRecipient recipient, QByteArray transaction) +{ + const payments::PaymentDetails& details = recipient.paymentRequest.getDetails(); + if (!details.has_payment_url()) + return; + + QNetworkRequest netRequest; + netRequest.setAttribute(QNetworkRequest::User, "PaymentACK"); + netRequest.setUrl(QString::fromStdString(details.payment_url())); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, BITCOIN_PAYMENTACK_CONTENTTYPE); + netRequest.setRawHeader("User-Agent", CLIENT_NAME.c_str()); + netRequest.setRawHeader("Accept", BITCOIN_PAYMENTACK_MIMETYPE); + + 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: + QString account = tr("Refund from") + QString(" ") + recipient.authenticatedMerchant; + std::string strAccount = account.toStdString(); + set<CTxDestination> refundAddresses = wallet->GetAccountAddresses(strAccount); + if (!refundAddresses.empty()) { + CScript s; s.SetDestination(*refundAddresses.begin()); + payments::Output* refund_to = payment.add_refund_to(); + refund_to->set_script(&s[0], s.size()); + } + else { + CPubKey newKey; + if (wallet->GetKeyFromPool(newKey)) { + CKeyID keyID = newKey.GetID(); + wallet->SetAddressBook(keyID, strAccount, "refund"); + + CScript s; s.SetDestination(keyID); + 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 + qDebug() << "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: + qDebug() << "PaymentServer::fetchPaymentACK : Error serializing payment message"; + } +} + +void PaymentServer::netRequestFinished(QNetworkReply* reply) +{ + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) + { + QString msg = QObject::tr("Error communicating with %1: %2") + .arg(reply->request().url().toString()) + .arg(reply->errorString()); + qDebug() << "PaymentServer::netRequestFinished : " << msg; + emit message(tr("Network request error"), msg, CClientUIInterface::MSG_ERROR); + return; + } + + QByteArray data = reply->readAll(); + + QString requestType = reply->request().attribute(QNetworkRequest::User).toString(); + if (requestType == "PaymentRequest") + { + PaymentRequestPlus request; + QList<SendCoinsRecipient> recipients; + if (request.parse(data) && processPaymentRequest(request, recipients)) { + foreach (const SendCoinsRecipient& recipient, recipients){ + emit receivedPaymentRequest(recipient); + } + } + else + qDebug() << "PaymentServer::netRequestFinished : Error processing payment request"; + return; + } + else if (requestType == "PaymentACK") + { + payments::PaymentACK paymentACK; + if (!paymentACK.ParseFromArray(data.data(), data.size())) + { + QString msg = QObject::tr("Bad response from server %1") + .arg(reply->request().url().toString()); + qDebug() << "PaymentServer::netRequestFinished : " << msg; + emit message(tr("Network request error"), msg, CClientUIInterface::MSG_ERROR); + } + else { + emit receivedPaymentACK(QString::fromStdString(paymentACK.memo())); + } + } +} + +void PaymentServer::reportSslErrors(QNetworkReply* reply, const QList<QSslError> &errs) +{ + Q_UNUSED(reply); + + QString errString; + foreach (const QSslError& err, errs) { + qDebug() << "PaymentServer::reportSslErrors : " << err; + errString += err.errorString() + "\n"; + } + emit message(tr("Network request error"), errString, CClientUIInterface::MSG_ERROR); +} + +void PaymentServer::setOptionsModel(OptionsModel *optionsModel) +{ + this->optionsModel = optionsModel; } |