Remove obsolete protobuf payment requests
This commit is contained in:
@@ -1,46 +0,0 @@
|
||||
//
|
||||
// Simple Bitcoin Payment Protocol messages
|
||||
//
|
||||
// Use fields 100+ for extensions;
|
||||
// to avoid conflicts, register extensions at:
|
||||
// https://en.bitcoin.it/wiki/Payment_Request
|
||||
//
|
||||
|
||||
package payments;
|
||||
option java_package = "org.bitcoin.protocols.payments";
|
||||
option java_outer_classname = "Protos";
|
||||
|
||||
// Generalized form of "send payment to this/these bitcoin addresses"
|
||||
message Output {
|
||||
optional uint64 amount = 1 [default = 0]; // amount is integer-number-of-satoshis
|
||||
required bytes script = 2; // usually one of the standard Script forms
|
||||
}
|
||||
message PaymentDetails {
|
||||
optional string network = 1 [default = "main"]; // "main" or "test"
|
||||
repeated Output outputs = 2; // Where payment should be sent
|
||||
required uint64 time = 3; // Timestamp; when payment request created
|
||||
optional uint64 expires = 4; // Timestamp; when this request should be considered invalid
|
||||
optional string memo = 5; // Human-readable description of request for the customer
|
||||
optional string payment_url = 6; // URL to send Payment and get PaymentACK
|
||||
optional bytes merchant_data = 7; // Arbitrary data to include in the Payment message
|
||||
}
|
||||
message PaymentRequest {
|
||||
optional uint32 payment_details_version = 1 [default = 1];
|
||||
optional string pki_type = 2 [default = "none"]; // none / x509+sha256 / x509+sha1
|
||||
optional bytes pki_data = 3; // depends on pki_type
|
||||
required bytes serialized_payment_details = 4; // PaymentDetails
|
||||
optional bytes signature = 5; // pki-dependent signature
|
||||
}
|
||||
message X509Certificates {
|
||||
repeated bytes certificate = 1; // DER-encoded X.509 certificate chain
|
||||
}
|
||||
message Payment {
|
||||
optional bytes merchant_data = 1; // From PaymentDetails.merchant_data
|
||||
repeated bytes transactions = 2; // Signed transactions that satisfy PaymentDetails.outputs
|
||||
repeated Output refund_to = 3; // Where to send refunds, if a refund is necessary
|
||||
optional string memo = 4; // Human-readable message for the merchant
|
||||
}
|
||||
message PaymentACK {
|
||||
required Payment payment = 1; // Payment message that triggered this ACK
|
||||
optional string memo = 2; // human-readable message for customer
|
||||
}
|
||||
+12
-184
@@ -3,214 +3,42 @@
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
//
|
||||
// Wraps dumb protocol buffer paymentRequest
|
||||
// with some extra methods
|
||||
//
|
||||
|
||||
#include "paymentrequestplus.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <openssl/x509_vfy.h>
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QSslCertificate>
|
||||
|
||||
using namespace std;
|
||||
|
||||
class SSLVerifyError : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
SSLVerifyError(std::string err) : std::runtime_error(err) {}
|
||||
};
|
||||
|
||||
bool PaymentRequestPlus::parse(const QByteArray& data)
|
||||
{
|
||||
bool parseOK = paymentRequest.ParseFromArray(data.data(), data.size());
|
||||
if (!parseOK) {
|
||||
qWarning() << "PaymentRequestPlus::parse : Error parsing payment request";
|
||||
return false;
|
||||
}
|
||||
if (paymentRequest.payment_details_version() > 1) {
|
||||
qWarning() << "PaymentRequestPlus::parse : Received up-version payment details, version=" << paymentRequest.payment_details_version();
|
||||
return false;
|
||||
}
|
||||
|
||||
parseOK = details.ParseFromString(paymentRequest.serialized_payment_details());
|
||||
if (!parseOK) {
|
||||
qWarning() << "PaymentRequestPlus::parse : Error parsing payment details";
|
||||
paymentRequest.Clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
Q_UNUSED(data);
|
||||
qWarning() << "PaymentRequestPlus::parse : BIP70 payment requests are no longer supported";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PaymentRequestPlus::SerializeToString(string* output) const
|
||||
bool PaymentRequestPlus::SerializeToString(std::string* output) const
|
||||
{
|
||||
return paymentRequest.SerializeToString(output);
|
||||
if (output)
|
||||
output->clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PaymentRequestPlus::IsInitialized() const
|
||||
{
|
||||
return paymentRequest.IsInitialized();
|
||||
return false;
|
||||
}
|
||||
|
||||
QString PaymentRequestPlus::getPKIType() const
|
||||
{
|
||||
if (!IsInitialized()) return QString("none");
|
||||
return QString::fromStdString(paymentRequest.pki_type());
|
||||
return QString("none");
|
||||
}
|
||||
|
||||
bool PaymentRequestPlus::getMerchant(X509_STORE* certStore, QString& merchant) const
|
||||
{
|
||||
Q_UNUSED(certStore);
|
||||
merchant.clear();
|
||||
|
||||
if (!IsInitialized())
|
||||
return false;
|
||||
|
||||
// One day we'll support more PKI types, but just
|
||||
// x509 for now:
|
||||
const EVP_MD* digestAlgorithm = NULL;
|
||||
if (paymentRequest.pki_type() == "x509+sha256") {
|
||||
digestAlgorithm = EVP_sha256();
|
||||
} else if (paymentRequest.pki_type() == "x509+sha1") {
|
||||
digestAlgorithm = EVP_sha1();
|
||||
} else if (paymentRequest.pki_type() == "none") {
|
||||
qWarning() << "PaymentRequestPlus::getMerchant : Payment request: pki_type == none";
|
||||
return false;
|
||||
} else {
|
||||
qWarning() << "PaymentRequestPlus::getMerchant : Payment request: unknown pki_type " << QString::fromStdString(paymentRequest.pki_type());
|
||||
return false;
|
||||
}
|
||||
|
||||
payments::X509Certificates certChain;
|
||||
if (!certChain.ParseFromString(paymentRequest.pki_data())) {
|
||||
qWarning() << "PaymentRequestPlus::getMerchant : Payment request: error parsing pki_data";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<X509*> certs;
|
||||
const QDateTime currentTime = QDateTime::currentDateTime();
|
||||
for (int i = 0; i < certChain.certificate_size(); i++) {
|
||||
QByteArray certData(certChain.certificate(i).data(), certChain.certificate(i).size());
|
||||
QSslCertificate qCert(certData, QSsl::Der);
|
||||
if (currentTime < qCert.effectiveDate() || currentTime > qCert.expiryDate()) {
|
||||
qWarning() << "PaymentRequestPlus::getMerchant : Payment request: certificate expired or not yet active: " << qCert;
|
||||
return false;
|
||||
}
|
||||
if (qCert.isBlacklisted()) {
|
||||
qWarning() << "PaymentRequestPlus::getMerchant : Payment request: certificate blacklisted: " << qCert;
|
||||
return false;
|
||||
}
|
||||
const unsigned char *data = (const unsigned char *)certChain.certificate(i).data();
|
||||
X509 *cert = d2i_X509(nullptr, &data, certChain.certificate(i).size());
|
||||
if (cert)
|
||||
certs.push_back(cert);
|
||||
}
|
||||
if (certs.empty()) {
|
||||
qWarning() << "PaymentRequestPlus::getMerchant : Payment request: empty certificate chain";
|
||||
return false;
|
||||
}
|
||||
|
||||
// The first cert is the signing cert, the rest are untrusted certs that chain
|
||||
// to a valid root authority. OpenSSL needs them separately.
|
||||
STACK_OF(X509)* chain =
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
|
||||
reinterpret_cast<STACK_OF(X509)*>(OPENSSL_sk_new_null());
|
||||
#else
|
||||
sk_X509_new_null();
|
||||
#endif
|
||||
for (int i = certs.size() - 1; i > 0; i--) {
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
|
||||
OPENSSL_sk_push(reinterpret_cast<OPENSSL_STACK*>(chain), reinterpret_cast<void*>(certs[i]));
|
||||
#else
|
||||
sk_X509_push(chain, certs[i]);
|
||||
#endif
|
||||
}
|
||||
X509* signing_cert = certs[0];
|
||||
|
||||
// Now create a "store context", which is a single use object for checking,
|
||||
// load the signing cert into it and verify.
|
||||
X509_STORE_CTX* store_ctx = X509_STORE_CTX_new();
|
||||
if (!store_ctx) {
|
||||
qWarning() << "PaymentRequestPlus::getMerchant : Payment request: error creating X509_STORE_CTX";
|
||||
return false;
|
||||
}
|
||||
|
||||
char* website = NULL;
|
||||
bool fResult = true;
|
||||
try {
|
||||
if (!X509_STORE_CTX_init(store_ctx, certStore, signing_cert, chain)) {
|
||||
int error = X509_STORE_CTX_get_error(store_ctx);
|
||||
throw SSLVerifyError(X509_verify_cert_error_string(error));
|
||||
}
|
||||
|
||||
// Now do the verification!
|
||||
int result = X509_verify_cert(store_ctx);
|
||||
if (result != 1) {
|
||||
int error = X509_STORE_CTX_get_error(store_ctx);
|
||||
throw SSLVerifyError(X509_verify_cert_error_string(error));
|
||||
}
|
||||
X509_NAME* certname = X509_get_subject_name(signing_cert);
|
||||
|
||||
// Valid cert; check signature:
|
||||
payments::PaymentRequest rcopy(paymentRequest); // Copy
|
||||
rcopy.set_signature(std::string(""));
|
||||
std::string data_to_verify; // Everything but the signature
|
||||
rcopy.SerializeToString(&data_to_verify);
|
||||
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
|
||||
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
|
||||
if (!ctx) throw SSLVerifyError("Error allocating OpenSSL context.");
|
||||
#else
|
||||
EVP_MD_CTX _ctx;
|
||||
EVP_MD_CTX *ctx;
|
||||
ctx = &_ctx;
|
||||
EVP_MD_CTX_init(ctx);
|
||||
#endif
|
||||
EVP_PKEY* pubkey = X509_get_pubkey(signing_cert);
|
||||
if (!EVP_VerifyInit_ex(ctx, digestAlgorithm, NULL) ||
|
||||
!EVP_VerifyUpdate(ctx, data_to_verify.data(), data_to_verify.size()) ||
|
||||
!EVP_VerifyFinal(ctx, (const unsigned char*)paymentRequest.signature().data(), paymentRequest.signature().size(), pubkey)) {
|
||||
throw SSLVerifyError("Bad signature, invalid PaymentRequest.");
|
||||
}
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
|
||||
EVP_MD_CTX_free(ctx);
|
||||
#endif
|
||||
|
||||
// OpenSSL API for getting human printable strings from certs is baroque.
|
||||
int textlen = X509_NAME_get_text_by_NID(certname, NID_commonName, NULL, 0);
|
||||
website = new char[textlen + 1];
|
||||
if (X509_NAME_get_text_by_NID(certname, NID_commonName, website, textlen + 1) == textlen && textlen > 0) {
|
||||
merchant = website;
|
||||
} else {
|
||||
throw SSLVerifyError("Bad certificate, missing common name.");
|
||||
}
|
||||
// TODO: detect EV certificates and set merchant = business name instead of unfriendly NID_commonName ?
|
||||
} catch (SSLVerifyError& err) {
|
||||
fResult = false;
|
||||
qWarning() << "PaymentRequestPlus::getMerchant : SSL error: " << err.what();
|
||||
}
|
||||
|
||||
if (website)
|
||||
delete[] website;
|
||||
X509_STORE_CTX_free(store_ctx);
|
||||
for (unsigned int i = 0; i < certs.size(); i++)
|
||||
X509_free(certs[i]);
|
||||
|
||||
return fResult;
|
||||
return false;
|
||||
}
|
||||
|
||||
QList<std::pair<CScript, CAmount> > PaymentRequestPlus::getPayTo() const
|
||||
{
|
||||
QList<std::pair<CScript, CAmount> > result;
|
||||
for (int i = 0; i < details.outputs_size(); i++) {
|
||||
const unsigned char* scriptStr = (const unsigned char*)details.outputs(i).script().data();
|
||||
CScript s(scriptStr, scriptStr + details.outputs(i).script().size());
|
||||
|
||||
result.append(make_pair(s, details.outputs(i).amount()));
|
||||
}
|
||||
return result;
|
||||
return QList<std::pair<CScript, CAmount> >();
|
||||
}
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
#ifndef BITCOIN_QT_PAYMENTREQUESTPLUS_H
|
||||
#define BITCOIN_QT_PAYMENTREQUESTPLUS_H
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#include "paymentrequest.pb.h"
|
||||
#pragma GCC diagnostic pop
|
||||
#include "amount.h"
|
||||
#include "script/script.h"
|
||||
|
||||
#include "base58.h"
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <openssl/x509.h>
|
||||
|
||||
@@ -19,11 +18,9 @@
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
|
||||
//
|
||||
// Wraps dumb protocol buffer paymentRequest
|
||||
// with extra methods
|
||||
//
|
||||
|
||||
// BIP70 payment requests were removed from the wallet to eliminate the
|
||||
// obsolete protobuf dependency. The class stays as a compatibility shim for
|
||||
// serialized wallet/order-form code paths that check for payment requests.
|
||||
class PaymentRequestPlus
|
||||
{
|
||||
public:
|
||||
@@ -31,21 +28,10 @@ public:
|
||||
|
||||
bool parse(const QByteArray& data);
|
||||
bool SerializeToString(std::string* output) const;
|
||||
|
||||
bool IsInitialized() const;
|
||||
QString getPKIType() const;
|
||||
// Returns true if merchant's identity is authenticated, and
|
||||
// returns human-readable merchant identity in merchant
|
||||
bool getMerchant(X509_STORE* certStore, QString& merchant) const;
|
||||
|
||||
// Returns list of outputs, amount
|
||||
QList<std::pair<CScript, CAmount> > getPayTo() const;
|
||||
|
||||
const payments::PaymentDetails& getDetails() const { return details; }
|
||||
|
||||
private:
|
||||
payments::PaymentRequest paymentRequest;
|
||||
payments::PaymentDetails details;
|
||||
};
|
||||
|
||||
#endif // BITCOIN_QT_PAYMENTREQUESTPLUS_H
|
||||
|
||||
+29
-241
@@ -48,14 +48,7 @@ using namespace std;
|
||||
|
||||
const int BITCOIN_IPC_CONNECT_TIMEOUT = 1000; // milliseconds
|
||||
const QString BITCOIN_IPC_PREFIX("agrarian:");
|
||||
// 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/agrarian-payment";
|
||||
const char* BIP71_MIMETYPE_PAYMENTACK = "application/agrarian-paymentack";
|
||||
const char* BIP71_MIMETYPE_PAYMENTREQUEST = "application/agrarian-paymentrequest";
|
||||
// BIP70 max payment request size in bytes (DoS protection)
|
||||
// BIP70 max payment request size in bytes (kept for compatibility tests).
|
||||
const qint64 BIP70_MAX_PAYMENTREQUEST_SIZE = 50000;
|
||||
|
||||
struct X509StoreDeleter {
|
||||
@@ -210,16 +203,7 @@ void PaymentServer::ipcParseCommandLine(int argc, char* argv[])
|
||||
}
|
||||
} else if (QFile::exists(arg)) // Filename
|
||||
{
|
||||
savedPaymentRequests.append(arg);
|
||||
|
||||
PaymentRequestPlus request;
|
||||
if (readPaymentRequestFromFile(arg, request)) {
|
||||
if (request.getDetails().network() == "main") {
|
||||
SelectParams(CBaseChainParams::MAIN);
|
||||
} else if (request.getDetails().network() == "test") {
|
||||
SelectParams(CBaseChainParams::TESTNET);
|
||||
}
|
||||
}
|
||||
qWarning() << "PaymentServer::ipcSendCommandLine : BIP70 payment request files are no longer supported: " << arg;
|
||||
} 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.
|
||||
@@ -271,10 +255,6 @@ PaymentServer::PaymentServer(QObject* parent, bool startLocalServer) : QObject(p
|
||||
netManager(0),
|
||||
optionsModel(0)
|
||||
{
|
||||
// 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 agrarian: links
|
||||
// other OSes: helpful when dealing with payment request files (in the future)
|
||||
@@ -295,14 +275,12 @@ PaymentServer::PaymentServer(QObject* parent, bool startLocalServer) : QObject(p
|
||||
tr("Cannot start agrarian: click-to-pay handler"));
|
||||
} else {
|
||||
connect(uriServer, SIGNAL(newConnection()), this, SLOT(handleURIConnection()));
|
||||
connect(this, SIGNAL(receivedPaymentACK(QString)), this, SLOT(handlePaymentACK(QString)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PaymentServer::~PaymentServer()
|
||||
{
|
||||
google::protobuf::ShutdownProtobufLibrary();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -374,21 +352,10 @@ void PaymentServer::handleURIOrFile(const QString& s)
|
||||
QUrlQuery uri((QUrl(s)));
|
||||
if (uri.hasQueryItem("r")) // payment request URI
|
||||
{
|
||||
QByteArray temp;
|
||||
temp.append(uri.queryItemValue("r").toUtf8());
|
||||
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;
|
||||
emit message(tr("URI handling"),
|
||||
tr("Payment request fetch URL is invalid: %1").arg(fetchUrl.toString()),
|
||||
CClientUIInterface::ICON_WARNING);
|
||||
}
|
||||
|
||||
qWarning() << "PaymentServer::handleURIOrFile : BIP70 payment request URLs are no longer supported.";
|
||||
emit message(tr("URI handling"),
|
||||
tr("BIP70 payment request URLs are no longer supported. Use a standard agrarian: URI with address and amount."),
|
||||
CClientUIInterface::ICON_WARNING);
|
||||
return;
|
||||
} else // normal URI
|
||||
{
|
||||
@@ -411,15 +378,10 @@ void PaymentServer::handleURIOrFile(const QString& s)
|
||||
|
||||
if (QFile::exists(s)) // payment request file
|
||||
{
|
||||
PaymentRequestPlus request;
|
||||
SendCoinsRecipient recipient;
|
||||
if (!readPaymentRequestFromFile(s, request)) {
|
||||
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))
|
||||
emit receivedPaymentRequest(recipient);
|
||||
|
||||
qWarning() << "PaymentServer::handleURIOrFile : BIP70 payment request files are no longer supported: " << s;
|
||||
emit message(tr("Payment request file handling"),
|
||||
tr("BIP70 payment request files are no longer supported. Use a standard agrarian: URI instead."),
|
||||
CClientUIInterface::ICON_WARNING);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -451,219 +413,45 @@ void PaymentServer::handleURIConnection()
|
||||
//
|
||||
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;
|
||||
}
|
||||
Q_UNUSED(filename);
|
||||
Q_UNUSED(request);
|
||||
|
||||
// BIP70 DoS protection
|
||||
if (f.size() > BIP70_MAX_PAYMENTREQUEST_SIZE) {
|
||||
qWarning() << QString("PaymentServer::%1: Payment request %2 is too large (%3 bytes, allowed %4 bytes).")
|
||||
.arg(__func__)
|
||||
.arg(filename)
|
||||
.arg(f.size())
|
||||
.arg(BIP70_MAX_PAYMENTREQUEST_SIZE);
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray data = f.readAll();
|
||||
|
||||
return request.parse(data);
|
||||
qWarning() << "PaymentServer::readPaymentRequestFromFile : BIP70 payment request files are no longer supported.";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PaymentServer::processPaymentRequest(PaymentRequestPlus& request, SendCoinsRecipient& recipient)
|
||||
{
|
||||
if (!optionsModel)
|
||||
return false;
|
||||
Q_UNUSED(request);
|
||||
Q_UNUSED(recipient);
|
||||
|
||||
if (request.IsInitialized()) {
|
||||
const payments::PaymentDetails& details = request.getDetails();
|
||||
|
||||
// Payment request network matches client network?
|
||||
if (details.network() != Params().NetworkIDString()) {
|
||||
emit message(tr("Payment request rejected"), tr("Payment request network doesn't match client network."),
|
||||
CClientUIInterface::MSG_ERROR);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Expired payment request?
|
||||
if (details.has_expires() && (int64_t)details.expires() < GetTime()) {
|
||||
emit message(tr("Payment request rejected"), tr("Payment request has expired."),
|
||||
CClientUIInterface::MSG_ERROR);
|
||||
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
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 auto& sendingTo : sendingTos) {
|
||||
// Extract and check destination addresses
|
||||
CTxDestination dest;
|
||||
if (ExtractDestination(sendingTo.first, dest)) {
|
||||
// Append destination address
|
||||
addresses.append(QString::fromStdString(CBitcoinAddress(dest).ToString()));
|
||||
} else if (!recipient.authenticatedMerchant.isEmpty()) {
|
||||
// Insecure payments to custom agrarian 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 rejected"),
|
||||
tr("Unverified payment requests to custom payment scripts are unsupported."),
|
||||
CClientUIInterface::MSG_ERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract and check amounts
|
||||
CTxOut txOut(sendingTo.second, sendingTo.first);
|
||||
if (txOut.IsDust(::minRelayTxFee)) {
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
emit message(tr("Payment request rejected"),
|
||||
tr("BIP70 payment requests are no longer supported."),
|
||||
CClientUIInterface::ICON_WARNING);
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
Q_UNUSED(url);
|
||||
|
||||
emit message(tr("Payment request rejected"),
|
||||
tr("BIP70 payment request URLs are no longer supported."),
|
||||
CClientUIInterface::ICON_WARNING);
|
||||
}
|
||||
|
||||
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, 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:
|
||||
QString account = tr("Refund from %1").arg(recipient.authenticatedMerchant);
|
||||
std::string strAccount = account.toStdString();
|
||||
set<CTxDestination> refundAddresses = wallet->GetAccountAddresses(strAccount);
|
||||
if (!refundAddresses.empty()) {
|
||||
CScript s = GetScriptForDestination(*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 = GetScriptForDestination(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.
|
||||
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";
|
||||
}
|
||||
Q_UNUSED(wallet);
|
||||
Q_UNUSED(recipient);
|
||||
Q_UNUSED(transaction);
|
||||
}
|
||||
|
||||
void PaymentServer::netRequestFinished(QNetworkReply* reply)
|
||||
{
|
||||
reply->deleteLater();
|
||||
|
||||
// BIP70 DoS protection
|
||||
if (reply->size() > BIP70_MAX_PAYMENTREQUEST_SIZE) {
|
||||
QString msg = 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);
|
||||
|
||||
qWarning() << QString("PaymentServer::%1:").arg(__func__) << msg;
|
||||
emit message(tr("Payment request DoS protection"), msg, 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;
|
||||
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";
|
||||
emit message(tr("Payment request error"),
|
||||
tr("Payment request cannot be parsed!"),
|
||||
CClientUIInterface::MSG_ERROR);
|
||||
} else if (processPaymentRequest(request, recipient))
|
||||
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;
|
||||
emit message(tr("Payment request error"), msg, CClientUIInterface::MSG_ERROR);
|
||||
} else {
|
||||
emit receivedPaymentACK(GUIUtil::HtmlEscape(paymentACK.memo()));
|
||||
}
|
||||
}
|
||||
qWarning() << "PaymentServer::netRequestFinished : Ignoring unexpected network reply after BIP70 removal.";
|
||||
}
|
||||
|
||||
void PaymentServer::reportSslErrors(QNetworkReply* reply, const QList<QSslError>& errs)
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "paymentservertests.h"
|
||||
|
||||
#include "optionsmodel.h"
|
||||
#include "paymentrequestdata.h"
|
||||
|
||||
#include "random.h"
|
||||
#include "util.h"
|
||||
@@ -65,50 +64,14 @@ void PaymentServerTests::paymentServerTests()
|
||||
SelectParams(CBaseChainParams::MAIN);
|
||||
OptionsModel optionsModel;
|
||||
PaymentServer* server = new PaymentServer(NULL, false);
|
||||
X509_STORE* caStore = X509_STORE_new();
|
||||
X509_STORE_add_cert(caStore, parse_b64der_cert(caCert_BASE64));
|
||||
PaymentServer::LoadRootCAs(caStore);
|
||||
server->setOptionsModel(&optionsModel);
|
||||
server->uiReady();
|
||||
|
||||
// Now feed PaymentRequests to server, and observe signals it produces:
|
||||
std::vector<unsigned char> data = DecodeBase64(paymentrequest1_BASE64);
|
||||
// BIP70 payment requests are intentionally unsupported in Agrarian 2.0.
|
||||
// Standard agrarian: URI handling remains covered by uritests.
|
||||
std::vector<unsigned char> data(128, 0);
|
||||
SendCoinsRecipient r = handleRequest(server, data);
|
||||
QString merchant;
|
||||
r.paymentRequest.getMerchant(caStore, merchant);
|
||||
QCOMPARE(merchant, QString("testmerchant.org"));
|
||||
|
||||
// Version of the above, with an expired certificate:
|
||||
data = DecodeBase64(paymentrequest2_BASE64);
|
||||
r = handleRequest(server, data);
|
||||
r.paymentRequest.getMerchant(caStore, merchant);
|
||||
QCOMPARE(merchant, QString(""));
|
||||
|
||||
// Long certificate chain:
|
||||
data = DecodeBase64(paymentrequest3_BASE64);
|
||||
r = handleRequest(server, data);
|
||||
r.paymentRequest.getMerchant(caStore, merchant);
|
||||
QCOMPARE(merchant, QString("testmerchant8.org"));
|
||||
|
||||
// Long certificate chain, with an expired certificate in the middle:
|
||||
data = DecodeBase64(paymentrequest4_BASE64);
|
||||
r = handleRequest(server, data);
|
||||
r.paymentRequest.getMerchant(caStore, merchant);
|
||||
QCOMPARE(merchant, QString(""));
|
||||
|
||||
// Validly signed, but by a CA not in our root CA list:
|
||||
data = DecodeBase64(paymentrequest5_BASE64);
|
||||
r = handleRequest(server, data);
|
||||
r.paymentRequest.getMerchant(caStore, merchant);
|
||||
QCOMPARE(merchant, QString(""));
|
||||
|
||||
// Try again with no root CA's, verifiedMerchant should be empty:
|
||||
caStore = X509_STORE_new();
|
||||
PaymentServer::LoadRootCAs(caStore);
|
||||
data = DecodeBase64(paymentrequest1_BASE64);
|
||||
r = handleRequest(server, data);
|
||||
r.paymentRequest.getMerchant(caStore, merchant);
|
||||
QCOMPARE(merchant, QString(""));
|
||||
QCOMPARE(r.paymentRequest.IsInitialized(), false);
|
||||
|
||||
unsigned long lDoSProtectionTrigger = (unsigned long) BIP70_MAX_PAYMENTREQUEST_SIZE + 1;
|
||||
std::string randData(lDoSProtectionTrigger, '\0');
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
#include "bitcoinunits.h"
|
||||
#include "guiutil.h"
|
||||
#include "paymentserver.h"
|
||||
#include "transactionrecord.h"
|
||||
|
||||
#include "base58.h"
|
||||
#include "db.h"
|
||||
#include "main.h"
|
||||
#include "script/script.h"
|
||||
#include "swifttx.h"
|
||||
#include "timedata.h"
|
||||
#include "guiinterface.h"
|
||||
#include "util.h"
|
||||
@@ -258,19 +258,6 @@ QString TransactionDesc::toHTML(CWallet* wallet, CWalletTx& wtx, TransactionReco
|
||||
if (r.first == "Message")
|
||||
strHTML += "<br><b>" + tr("Message") + ":</b><br>" + GUIUtil::HtmlEscape(r.second, true) + "<br>";
|
||||
|
||||
//
|
||||
// PaymentRequest info:
|
||||
//
|
||||
for (const auto& r : wtx.vOrderForm) {
|
||||
if (r.first == "PaymentRequest") {
|
||||
PaymentRequestPlus req;
|
||||
req.parse(QByteArray::fromRawData(r.second.data(), r.second.size()));
|
||||
QString merchant;
|
||||
if (req.getMerchant(PaymentServer::getCertStore(), merchant))
|
||||
strHTML += "<b>" + tr("Merchant") + ":</b> " + GUIUtil::HtmlEscape(merchant) + "<br>";
|
||||
}
|
||||
}
|
||||
|
||||
if (wtx.IsCoinBase()) {
|
||||
quint32 numBlocksToMaturity = Params().COINBASE_MATURITY() + 1;
|
||||
strHTML += "<br>" + tr("Generated coins must mature %1 blocks before they can be spent. When you generated this block, it was broadcast to the network to be added to the block chain. If it fails to get into the chain, its state will change to \"not accepted\" and it won't be spendable. This may occasionally happen if another node generates a block within a few seconds of yours.").arg(QString::number(numBlocksToMaturity)) + "<br>";
|
||||
|
||||
+18
-45
@@ -284,36 +284,19 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
|
||||
|
||||
// Pre-check input data for validity
|
||||
foreach (const SendCoinsRecipient& rcp, recipients) {
|
||||
if (rcp.paymentRequest.IsInitialized()) { // PaymentRequest...
|
||||
CAmount subtotal = 0;
|
||||
const payments::PaymentDetails& details = rcp.paymentRequest.getDetails();
|
||||
for (int i = 0; i < details.outputs_size(); i++) {
|
||||
const payments::Output& out = details.outputs(i);
|
||||
if (out.amount() <= 0) continue;
|
||||
subtotal += out.amount();
|
||||
const unsigned char* scriptStr = (const unsigned char*)out.script().data();
|
||||
CScript scriptPubKey(scriptStr, scriptStr + out.script().size());
|
||||
vecSend.push_back(std::pair<CScript, CAmount>(scriptPubKey, out.amount()));
|
||||
}
|
||||
if (subtotal <= 0) {
|
||||
return InvalidAmount;
|
||||
}
|
||||
total += subtotal;
|
||||
} else { // User-entered agrarian address / amount:
|
||||
if (!validateAddress(rcp.address)) {
|
||||
return InvalidAddress;
|
||||
}
|
||||
if (rcp.amount <= 0) {
|
||||
return InvalidAmount;
|
||||
}
|
||||
setAddress.insert(rcp.address);
|
||||
++nAddresses;
|
||||
|
||||
CScript scriptPubKey = GetScriptForDestination(CBitcoinAddress(rcp.address.toStdString()).Get());
|
||||
vecSend.push_back(std::pair<CScript, CAmount>(scriptPubKey, rcp.amount));
|
||||
|
||||
total += rcp.amount;
|
||||
if (!validateAddress(rcp.address)) {
|
||||
return InvalidAddress;
|
||||
}
|
||||
if (rcp.amount <= 0) {
|
||||
return InvalidAmount;
|
||||
}
|
||||
setAddress.insert(rcp.address);
|
||||
++nAddresses;
|
||||
|
||||
CScript scriptPubKey = GetScriptForDestination(CBitcoinAddress(rcp.address.toStdString()).Get());
|
||||
vecSend.push_back(std::pair<CScript, CAmount>(scriptPubKey, rcp.amount));
|
||||
|
||||
total += rcp.amount;
|
||||
}
|
||||
if (setAddress.size() != nAddresses) {
|
||||
return DuplicateAddress;
|
||||
@@ -381,15 +364,9 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction& tran
|
||||
CWalletTx* newTx = transaction.getTransaction();
|
||||
QList<SendCoinsRecipient> recipients = transaction.getRecipients();
|
||||
|
||||
// Store PaymentRequests in wtx.vOrderForm in wallet.
|
||||
// Store message from normal agrarian:URI (agrarian:XyZ...?message=example).
|
||||
foreach (const SendCoinsRecipient& rcp, recipients) {
|
||||
if (rcp.paymentRequest.IsInitialized()) {
|
||||
std::string key("PaymentRequest");
|
||||
std::string value;
|
||||
rcp.paymentRequest.SerializeToString(&value);
|
||||
newTx->vOrderForm.push_back(make_pair(key, value));
|
||||
} else if (!rcp.message.isEmpty()) // Message from normal agrarian:URI (agrarian:XyZ...?message=example)
|
||||
{
|
||||
if (!rcp.message.isEmpty()) {
|
||||
newTx->vOrderForm.push_back(make_pair("Message", rcp.message.toStdString()));
|
||||
}
|
||||
}
|
||||
@@ -410,15 +387,11 @@ WalletModel::SendCoinsReturn WalletModel::sendCoins(WalletModelTransaction& tran
|
||||
// Add addresses / update labels that we've sent to to the address book,
|
||||
// and emit coinsSent signal for each recipient
|
||||
foreach (const SendCoinsRecipient& rcp, transaction.getRecipients()) {
|
||||
// Don't touch the address book when we have a payment request
|
||||
if (!rcp.paymentRequest.IsInitialized()) {
|
||||
std::string strAddress = rcp.address.toStdString();
|
||||
CTxDestination dest = CBitcoinAddress(strAddress).Get();
|
||||
std::string strLabel = rcp.label.toStdString();
|
||||
|
||||
std::string strAddress = rcp.address.toStdString();
|
||||
CTxDestination dest = CBitcoinAddress(strAddress).Get();
|
||||
std::string strLabel = rcp.label.toStdString();
|
||||
|
||||
updateAddressBookLabels(dest, strLabel, "send");
|
||||
}
|
||||
updateAddressBookLabels(dest, strLabel, "send");
|
||||
emit coinsSent(wallet, rcp, transaction_array);
|
||||
}
|
||||
checkBalanceChanged(); // update balance immediately, otherwise there could be a short noticeable delay until pollBalanceChanged hits
|
||||
|
||||
@@ -54,12 +54,11 @@ public:
|
||||
AvailableCoinsType inputType;
|
||||
bool useSwiftTX;
|
||||
CAmount amount;
|
||||
// If from a payment request, this is used for storing the memo
|
||||
// Message from a standard agrarian: URI.
|
||||
QString message;
|
||||
|
||||
// If from a payment request, paymentRequest.IsInitialized() will be true
|
||||
// Obsolete BIP70 compatibility field. The shim is intentionally never initialized.
|
||||
PaymentRequestPlus paymentRequest;
|
||||
// Empty if no authentication or invalid signature/cert/etc.
|
||||
QString authenticatedMerchant;
|
||||
|
||||
static const int CURRENT_VERSION = 1;
|
||||
@@ -74,8 +73,6 @@ public:
|
||||
std::string sLabel = label.toStdString();
|
||||
std::string sMessage = message.toStdString();
|
||||
std::string sPaymentRequest;
|
||||
if (!ser_action.ForRead() && paymentRequest.IsInitialized())
|
||||
paymentRequest.SerializeToString(&sPaymentRequest);
|
||||
std::string sAuthenticatedMerchant = authenticatedMerchant.toStdString();
|
||||
|
||||
READWRITE(this->nVersion);
|
||||
@@ -91,8 +88,6 @@ public:
|
||||
address = QString::fromStdString(sAddress);
|
||||
label = QString::fromStdString(sLabel);
|
||||
message = QString::fromStdString(sMessage);
|
||||
if (!sPaymentRequest.empty())
|
||||
paymentRequest.parse(QByteArray::fromRawData(sPaymentRequest.data(), sPaymentRequest.size()));
|
||||
authenticatedMerchant = QString::fromStdString(sAuthenticatedMerchant);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user