diff options
author | Gavin Andresen <gavinandresen@gmail.com> | 2013-08-22 01:54:28 -0700 |
---|---|---|
committer | Gavin Andresen <gavinandresen@gmail.com> | 2013-08-22 01:54:28 -0700 |
commit | e62f8d72f349aec0865268c089ae99fedd314af1 (patch) | |
tree | 7735f34781f1ced27553b202bceaa74048018636 /src/qt/paymentserver.cpp | |
parent | e4348d2179b5083769582b3036f40902b0122bbf (diff) | |
parent | a41d5fe01947f2f878c055670986a165af800f9a (diff) |
Merge pull request #2539 from gavinandresen/paymentrequest
Payment Protocol Work
Diffstat (limited to 'src/qt/paymentserver.cpp')
-rw-r--r-- | src/qt/paymentserver.cpp | 527 |
1 files changed, 491 insertions, 36 deletions
diff --git a/src/qt/paymentserver.cpp b/src/qt/paymentserver.cpp index 0d31f24a13..a9f71315a9 100644 --- a/src/qt/paymentserver.cpp +++ b/src/qt/paymentserver.cpp @@ -2,30 +2,63 @@ // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include "paymentserver.h" - -#include "guiconstants.h" -#include "ui_interface.h" -#include "util.h" - #include <QApplication> #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 +#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" + using namespace boost; const int BITCOIN_IPC_CONNECT_TIMEOUT = 1000; // milliseconds const QString BITCOIN_IPC_PREFIX("bitcoin:"); +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: // testnet / non-testnet @@ -45,11 +78,99 @@ static QString ipcServerName() } // -// This stores payment requests received before +// We store payment URLs 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() << "Invalid certificate: " << cert.subjectInfo(QSslCertificate::CommonName); + } +} + +// +// Load openSSL's list of root certificate authorities // -static QStringList savedPaymentRequests; +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: 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. @@ -57,19 +178,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: + { + 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 + { + qDebug() << "Payment request file does not exist: " << argv[i]; + // 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. + } } - foreach (const QString& arg, savedPaymentRequests) + foreach (const QString& r, savedPaymentRequests) { QLocalSocket* socket = new QLocalSocket(); socket->connectToServer(ipcServerName(), QIODevice::WriteOnly); @@ -79,7 +235,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(); @@ -92,50 +248,148 @@ bool PaymentServer::ipcSendCommandLine() return fResult; } -PaymentServer::PaymentServer(QApplication* parent) : QObject(parent), saveURIs(true) +PaymentServer::PaymentServer(QObject* parent, + bool startLocalServer) : QObject(parent), saveURIs(true) { + // 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 the mac (sent when you click bitcoin: links) - parent->installEventFilter(this); + 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() << "Cannot start bitcoin: click-to-pay handler"; + else + connect(uriServer, SIGNAL(newConnection()), this, SLOT(handleURIConnection())); + } + + // netManager is null until uiReady() is called + netManager = NULL; } -bool PaymentServer::eventFilter(QObject *object, QEvent *event) +PaymentServer::~PaymentServer() +{ + google::protobuf::ShutdownProtobufLibrary(); +} + +// +// 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: 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(const OptionsModel& options) +{ + if (netManager != NULL) + delete netManager; + + // netManager is used to fetch paymentrequests given in bitcoin: URI's + netManager = new QNetworkAccessManager(this); + + // Use proxy settings from options: + QString proxyIP; + quint16 proxyPort; + if (options.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 url((QUrl(s))); +#else + QUrl url(s); +#endif + if (url.hasQueryItem("request")) + { + QByteArray temp; temp.append(url.queryItemValue("request")); + QString decoded = QUrl::fromPercentEncoding(temp); + QUrl fetchUrl(decoded, QUrl::StrictMode); + + if (fDebug) qDebug() << "PaymentServer::fetchRequest " << fetchUrl; + + if (fetchUrl.isValid()) + fetchRequest(fetchUrl); + else + qDebug() << "PaymentServer: invalid url: " << fetchUrl; + return; + } + + SendCoinsRecipient recipient; + if (GUIUtil::parseBitcoinURI(s, &recipient)) + emit receivedPaymentRequest(recipient); + 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(); @@ -154,8 +408,209 @@ void PaymentServer::handleURIConnection() QString message; in >> message; - if (saveURIs) - savedPaymentRequests.append(message); - else - emit receivedURI(message); + handleURIOrFile(message); +} + +bool PaymentServer::readPaymentRequest(const QString& filename, PaymentRequestPlus& request) +{ + QFile f(filename); + if (!f.open(QIODevice::ReadOnly)) + { + qDebug() << "PaymentServer::readPaymentRequest fail 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) +{ + 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 message = QObject::tr("Requested payment amount (%1) too small") + .arg(BitcoinUnits::formatWithUnit(BitcoinUnits::BTC, sendingTo.second)); + qDebug() << message; + emit reportError(tr("Payment request error"), message, CClientUIInterface::MODAL); + 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() << "PaymentRequest 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()); +#if QT_VERSION < 0x050000 + recipients[i].label = Qt::escape(memo); +#else + recipients[i].label = memo.toHtmlEscaped(); +#endif + 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() << "PaymentRequest, 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 reportError(tr("Payment request error"), + tr("Insecure requests to custom payment scripts unsupported"), + CClientUIInterface::MODAL); + 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()); + 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, "application/bitcoin-payment"); + netRequest.setRawHeader("User-Agent", CLIENT_NAME.c_str()); + + 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, false)) { + 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() << "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() << "Error serializing payment message"; + } +} + +void +PaymentServer::netRequestFinished(QNetworkReply* reply) +{ + reply->deleteLater(); + if (reply->error() != QNetworkReply::NoError) + { + QString message = QObject::tr("Error communicating with %1: %2") + .arg(reply->request().url().toString()) + .arg(reply->errorString()); + qDebug() << message; + emit reportError(tr("Network request error"), message, CClientUIInterface::MODAL); + 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 PaymentRequest"; + return; + } + else if (requestType == "PaymentACK") + { + payments::PaymentACK paymentACK; + if (!paymentACK.ParseFromArray(data.data(), data.size())) + { + QString message = QObject::tr("Bad response from server %1") + .arg(reply->request().url().toString()); + qDebug() << message; + emit reportError(tr("Network request error"), message, CClientUIInterface::MODAL); + } + else { + emit receivedPaymentACK(QString::fromStdString(paymentACK.memo())); + } + } +} + +void +PaymentServer::reportSslErrors(QNetworkReply* reply, const QList<QSslError> &errs) +{ + QString errString; + foreach (const QSslError& err, errs) { + qDebug() << err; + errString += err.errorString() + "\n"; + } + emit reportError(tr("Network request error"), errString, CClientUIInterface::MODAL); } |