diff --git a/doc/fulcrum-example-config.conf b/doc/fulcrum-example-config.conf index f5bdb338..8005f3da 100644 --- a/doc/fulcrum-example-config.conf +++ b/doc/fulcrum-example-config.conf @@ -538,6 +538,17 @@ rpcpassword = hunter1 #bitcoind_throttle = 50 20 5 +# Bitcoin daemon RPC uses TLS (HTTPS) - 'bitcoind-tls' - DEFAULT: false +# +# If true, connect to the remote bitcoind via HTTPS rather than the usual HTTP. +# Historically, bitcoind supported only JSON-RPC over HTTP; however, some +# implementations such as bchd support HTTPS. If you are using Fulcrum with +# bchd, you either need to start bchd with the `notls` option, or you need to +# specify this option to Fulcrum. +# +#bitcoind-tls = false + + # Keep RocksDB Log Files - 'db_keep_log_file_num' - DEAFULT: 5 # # The maximum number of database log files to keep around on disk, per database. diff --git a/doc/fulcrum-quick-config.conf b/doc/fulcrum-quick-config.conf index 00470b61..99e31690 100644 --- a/doc/fulcrum-quick-config.conf +++ b/doc/fulcrum-quick-config.conf @@ -15,6 +15,10 @@ datadir = /path/to/a/dir # Windows: datadir = D:\FulcrumData\mainnet # rpcport= in your bitcoind .conf file. bitcoind = 127.0.0.1:8332 +# *OPTIONAL* Use this option to connect to bitcoind via HTTPS rather than the +# usual HTTP. This option typically is only used with `bchd`. +#bitcoind-tls = true + # *REQUIRED* This is the bitcoind RPC username you specified in your bitciond # .conf file. This corresponds to the rpcuser= from that file. rpcuser = Bob_The_Banker diff --git a/doc/unix-man-page.md b/doc/unix-man-page.md index 69761db3..9b8ba2eb 100644 --- a/doc/unix-man-page.md +++ b/doc/unix-man-page.md @@ -67,6 +67,9 @@ Once the server finishes synching it will behave like an ElectronX/ElectrumX ser -b, --bitcoind : Specify a to connect to the bitcoind rpc service. This is a required option, along with -u and -p. This hostname:port should be the same as you specified in your bitcoin.conf file under rpcbind- and rpcport-. +--bitcoind-tls +: If specified, connect to the remote bitcoind via HTTPS rather than the usual HTTP. Historically, bitcoind supported only JSON-RPC over HTTP; however, some implementations such as *bchd* support HTTPS. If you are using *fulcrum* with *bchd*, you either need to start *bchd* with the `notls` option, or you need to specify this option to *fulcrum*. + -u, --rpcuser : Specify a username to use for authenticating to bitcoind. This is a required option, along with -b and -p. This option should be the same username you specified in your bitcoind.conf file under rpcuser-. For security, you may omit this option from the command-line and use the RPCUSER environment variable instead (the CLI arg takes precedence if both are present). diff --git a/src/App.cpp b/src/App.cpp index 464759aa..2b425763 100644 --- a/src/App.cpp +++ b/src/App.cpp @@ -261,6 +261,12 @@ void App::parseArgs() " under rpcbind= and rpcport=."), QString("hostname:port"), }, + { "bitcoind-tls", + QString("If specified, connect to the remote bitcoind via HTTPS rather than the usual HTTP. Historically," + " bitcoind supported only JSON-RPC over HTTP; however, some implementations such as bchd support" + " HTTPS. If you are using " APPNAME " with bchd, you either need to start bchd with the `notls`" + " option, or you need to specify this option to " APPNAME "."), + }, { { "u", "rpcuser" }, QString("Specify a username to use for authenticating to bitcoind. This is a required option, along" " with -b and -p. This option should be the same username you specified in your bitcoind.conf file" @@ -513,6 +519,9 @@ void App::parseArgs() // parse bitcoind - conf.value is always unset if parser.value is set, hence this strange constrcution below (parser.value takes precedence) options->bitcoind = parseHostnamePortPair(conf.value("bitcoind", parser.value("b"))); + // --bitcoind-tls + if ((options->bitcoindUsesTls = parser.isSet("bitcoind-tls") || conf.boolValue("bitcoind-tls"))) + Util::AsyncOnObject(this, []{ Debug() << "config: bitcoind-tls = true"; }); // grab rpcuser options->rpcuser = conf.value("rpcuser", parser.isSet("u") ? parser.value("u") : std::getenv(RPCUSER)); // grab rpcpass diff --git a/src/BitcoinD.cpp b/src/BitcoinD.cpp index 72a9e231..1cb1e922 100644 --- a/src/BitcoinD.cpp +++ b/src/BitcoinD.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include @@ -33,9 +35,8 @@ namespace { }; } -BitcoinDMgr::BitcoinDMgr(const QString &hostName, quint16 port, - const QString &user, const QString &pass) - : Mgr(nullptr), IdMixin(newId()), hostName(hostName), port(port), user(user), pass(pass) +BitcoinDMgr::BitcoinDMgr(const QString &hostName, quint16 port, const QString &user, const QString &pass, bool useSsl) + : Mgr(nullptr), IdMixin(newId()), hostName(hostName), port(port), user(user), pass(pass), useSsl(useSsl) { setObjectName("BitcoinDMgr"); _thread.setObjectName(objectName()); @@ -54,7 +55,7 @@ void BitcoinDMgr::startup() { for (auto & client : clients) { // initial resolvedAddress may be invalid if user specified a hostname, in which case we will resolve it and // tell bitcoind's to update themselves and reconnect - client = std::make_unique(hostName, port, user, pass); + client = std::make_unique(hostName, port, user, pass, useSsl); // connect client to us -- TODO: figure out workflow: how requests for work and results will get dispatched connect(client.get(), &BitcoinD::gotMessage, this, &BitcoinDMgr::on_Message); @@ -581,8 +582,8 @@ auto BitcoinD::stats() const -> Stats return m; } -BitcoinD::BitcoinD(const QString &host, quint16 port, const QString & user, const QString &pass, qint64 maxBuffer_) - : RPC::HttpConnection(RPC::MethodMap{}, newId(), nullptr, maxBuffer_), host(host), port(port) +BitcoinD::BitcoinD(const QString &host, quint16 port, const QString & user, const QString &pass, bool useSsl_, qint64 maxBuffer_) + : RPC::HttpConnection(RPC::MethodMap{}, newId(), nullptr, maxBuffer_), host(host), port(port), useSsl(useSsl_) { static int N = 1; setObjectName(QString("BitcoinD.%1").arg(N++)); @@ -654,9 +655,30 @@ void BitcoinD::on_started() void BitcoinD::reconnect() { if (socket) delete socket; - socket = new QTcpSocket(this); - socketConnectSignals(); - socket->connectToHost(host, port); + if (useSsl) { + // remote bitcoind expects https (--bitcoind-tls CLI option); usually this is only for bchd + QSslSocket *ssl; + socket = ssl = new QSslSocket(this); + + auto conf = ssl->sslConfiguration(); + conf.setPeerVerifyMode(QSslSocket::PeerVerifyMode::VerifyNone); + conf.setProtocol(QSsl::SslProtocol::AnyProtocol); + ssl->setSslConfiguration(conf); + + socketConnectSignals(); + connect(ssl, qOverload &>(&QSslSocket::sslErrors), ssl, [ssl](auto errs) { + for (const auto & err : errs) + DebugM("Ignoring SSL error for ", ssl->peerName(), ": ", err.errorString()); + ssl->ignoreSslErrors(); + }); + + ssl->connectToHostEncrypted(host, port); + } else { + // regular http bitcoind (default) + socket = new QTcpSocket(this); + socketConnectSignals(); + socket->connectToHost(host, port); + } } void BitcoinD::on_connected() diff --git a/src/BitcoinD.h b/src/BitcoinD.h index 20d31f83..dc061ddd 100644 --- a/src/BitcoinD.h +++ b/src/BitcoinD.h @@ -51,7 +51,7 @@ class BitcoinDMgr : public Mgr, public IdMixin, public ThreadObjectMixin, public { Q_OBJECT public: - BitcoinDMgr(const QString &hostnameOrIP, quint16 port, const QString &user, const QString &pass); + BitcoinDMgr(const QString &hostnameOrIP, quint16 port, const QString &user, const QString &pass, bool useSsl); ~BitcoinDMgr() override; void startup() override; ///< from Mgr @@ -114,6 +114,7 @@ protected slots: const QString hostName; const quint16 port; const QString user, pass; + const bool useSsl; static constexpr int miniTimeout = 333, tinyTimeout = 167, medTimeout = 500, longTimeout = 1000; @@ -184,7 +185,8 @@ class BitcoinD : public RPC::HttpConnection, public ThreadObjectMixin /* NB: als /// This should work for now since we are on 32MiB max block size on BCH anyway right now. static constexpr qint64 BTCD_DEFAULT_MAX_BUFFER = 100'000'000; - explicit BitcoinD(const QString &host, quint16 port, const QString & user, const QString &pass, qint64 maxBuffer = BTCD_DEFAULT_MAX_BUFFER); + explicit BitcoinD(const QString &host, quint16 port, const QString & user, const QString &pass, bool useSsl, + qint64 maxBuffer = BTCD_DEFAULT_MAX_BUFFER); ~BitcoinD() override; using ThreadObjectMixin::start; @@ -223,6 +225,7 @@ class BitcoinD : public RPC::HttpConnection, public ThreadObjectMixin /* NB: als const QString host; const quint16 port; + const bool useSsl; std::atomic_bool badAuth = false, needAuth = true; }; diff --git a/src/Controller.cpp b/src/Controller.cpp index 0bd8cbf0..4d950079 100644 --- a/src/Controller.cpp +++ b/src/Controller.cpp @@ -61,7 +61,7 @@ void Controller::startup() // this may take a long time but normally this branch is not taken dumpScriptHashes(options->dumpScriptHashes); - bitcoindmgr = std::make_shared(options->bitcoind.first, options->bitcoind.second, options->rpcuser, options->rpcpassword); + bitcoindmgr = std::make_shared(options->bitcoind.first, options->bitcoind.second, options->rpcuser, options->rpcpassword, options->bitcoindUsesTls); { auto constexpr waitTimer = "wait4bitcoind", callProcessTimer = "callProcess"; int constexpr msgPeriod = 10000, // 10sec diff --git a/src/Options.cpp b/src/Options.cpp index 9d254894..1c114b5c 100644 --- a/src/Options.cpp +++ b/src/Options.cpp @@ -95,6 +95,7 @@ QVariantMap Options::toMap() const m["cert"] = certFile; m["key"] = keyFile; m["bitcoind"] = QString("%1:%2").arg(bitcoind.first).arg(bitcoind.second); + m["bitcoind-tls"] = bitcoindUsesTls; m["hasIPv6 listener"] = hasIPv6Listener; m["rpcuser"] = rpcuser.isNull() ? QVariant() : QVariant(""); m["rpcpassword"] = rpcpassword.isNull() ? QVariant() : QVariant(""); diff --git a/src/Options.h b/src/Options.h index b9b7d4fd..ca7b798f 100644 --- a/src/Options.h +++ b/src/Options.h @@ -71,6 +71,7 @@ struct Options { QSslKey sslKey; ///< this must be valid if we have any SSL or WSS interfaces. QString keyFile; ///< saved here for toMap() to remember what was specified in config file QPair bitcoind; ///< hostname, port pair. We resolve bitcoind's actual IP address each time if it's a hostname and not an IP address string. + bool bitcoindUsesTls = false; ///< CLI: --bitcoind-tls. If true, we will connect to the remote bitcoind via SSL/TLS. See BitcoinD.cpp. QString rpcuser, rpcpassword; QString datadir; ///< The directory to store the database. It exists and has appropriate permissions (otherwise the app would have quit on startup). /// If true, on db open/startup, we will perform some slow/paranoid db consistency checks