2019-12-28 23:46:05 +02:00
//
2019-12-28 23:48:24 +02:00
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
2019-12-28 23:46:05 +02:00
// Copyright (C) 2019-2020 Calin A. Culianu <calin.culianu@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (see LICENSE.txt). If not, see
// <https://www.gnu.org/licenses/>.
//
2019-04-23 02:31:49 +03:00
# include "App.h"
2019-05-01 18:19:17 +03:00
# include "BTC.h"
2019-11-18 00:21:34 +02:00
# include "Controller.h"
2019-11-12 22:14:04 +02:00
# include "Logger.h"
2019-11-08 17:11:58 +02:00
# include "Servers.h"
2019-11-12 22:14:04 +02:00
# include "Util.h"
2019-04-23 23:26:07 +03:00
# include <QCommandLineParser>
2019-11-28 23:58:24 +02:00
# include <QDir>
2019-04-23 23:26:07 +03:00
# include <QFile>
2019-11-28 23:58:24 +02:00
# include <QFileInfo>
2019-12-24 16:49:47 +02:00
# include <QSslSocket>
2019-11-12 22:14:04 +02:00
2019-11-07 23:55:13 +02:00
# include <csignal>
2019-11-28 23:58:24 +02:00
# include <cstdlib>
# include <list>
# include <tuple>
2019-04-23 23:26:07 +03:00
2019-11-13 11:32:52 +02:00
2019-04-23 02:31:49 +03:00
App : : App ( int argc , char * argv [ ] )
: QCoreApplication ( argc , argv )
{
2019-11-18 15:10:42 +02:00
register_MetaTypes ( ) ;
2019-11-18 00:21:34 +02:00
options = std : : make_shared < Options > ( ) ;
2019-12-24 16:49:47 +02:00
options - > interfaces = { { QHostAddress ( " 0.0.0.0 " ) , Options : : DEFAULT_PORT_TCP } } ; // start with default, will be cleared if -t specified
2019-04-23 02:31:49 +03:00
setApplicationName ( APPNAME ) ;
2019-04-23 23:26:07 +03:00
setApplicationVersion ( QString ( " %1 %2 " ) . arg ( VERSION ) . arg ( VERSION_EXTRA ) ) ;
2019-04-23 12:36:51 +03:00
2019-11-17 11:10:41 +02:00
_logger = std : : make_unique < ConsoleLogger > ( this ) ;
2019-04-23 23:26:07 +03:00
try {
parseArgs ( ) ;
2020-01-03 23:28:57 +02:00
} catch ( const std : : exception & e ) {
2019-11-18 00:21:34 +02:00
options - > syslogMode = true ; // suppress timestamp stuff
2019-04-23 23:26:07 +03:00
Error ( ) < < e . what ( ) ;
2020-01-03 23:28:57 +02:00
Log ( ) < < " Use the -h option to show help. " ;
2019-04-23 23:26:07 +03:00
std : : exit ( 1 ) ;
}
2019-11-18 00:21:34 +02:00
if ( options - > syslogMode ) {
2019-11-17 11:10:41 +02:00
_logger = std : : make_unique < SysLogger > ( this ) ;
2019-04-27 16:00:58 +03:00
}
2019-04-23 02:31:49 +03:00
connect ( this , & App : : aboutToQuit , this , & App : : cleanup ) ;
2019-11-09 01:55:48 +02:00
QTimer : : singleShot ( 0 , this , & App : : startup ) ; // register to run after app event loop start
2019-04-23 02:31:49 +03:00
}
App : : ~ App ( )
{
Debug ( ) < < " App d'tor " ;
2019-04-26 08:47:46 +03:00
Log ( ) < < " Shudown complete " ;
2019-04-23 02:31:49 +03:00
/// child objects will be auto-deleted
}
void App : : startup ( )
{
2019-11-27 19:11:57 +02:00
static const auto getBannerWithTimeStamp = [ ] {
QString ret ; {
QTextStream ts ( & ret , QIODevice : : WriteOnly | QIODevice : : Truncate ) ;
ts < < applicationName ( ) < < " " < < applicationVersion ( ) < < " - " < < QDateTime : : currentDateTime ( ) . toString ( " ddd MMM d, yyyy hh:mm:ss.zzz t " ) ;
} return ret ;
} ;
// print banner to log now
Log ( ) < < getBannerWithTimeStamp ( ) < < " - starting up ... " ;
2019-12-24 16:49:47 +02:00
// schedule print banner to log every hour so admin has an idea of how log timestamps correlate to wall clock time
2019-11-27 19:11:57 +02:00
callOnTimerSoon ( 60 * 60 * 1000 , " printTimeStamp " , [ ] { Log ( ) < < getBannerWithTimeStamp ( ) ; return true ; } , Qt : : TimerType : : VeryCoarseTimer ) ;
2019-11-11 11:55:10 +02:00
if ( ! Util : : isClockSteady ( ) ) {
2019-12-31 19:42:34 +02:00
Debug ( ) < < " High resolution clock provided by the std C++ library is not 'steady'. Log timestamps may drift if system time gets adjusted. " ;
2019-11-11 11:55:10 +02:00
} else {
Debug ( ) < < " High resolution clock: isSteady = true " ;
}
2019-04-23 02:31:49 +03:00
try {
2019-11-18 00:21:34 +02:00
BTC : : CheckBitcoinEndiannessAndOtherSanityChecks ( ) ;
2019-05-01 18:19:17 +03:00
2019-04-23 02:31:49 +03:00
auto gotsig = [ ] ( int sig ) {
2019-04-27 12:38:50 +03:00
static int ct = 0 ;
if ( ! ct + + ) {
Log ( ) < < " Got signal: " < < sig < < " , exiting ... " ;
app ( ) - > exit ( sig ) ;
} else if ( ct < 5 ) {
std : : printf ( " Duplicate signal %d already being handled, ignoring \n " , sig ) ;
} else {
2019-04-27 12:52:29 +03:00
std : : printf ( " Signal %d caught more than 5 times, aborting \n " , sig ) ;
2019-04-27 12:38:50 +03:00
std : : abort ( ) ;
}
2019-04-23 02:31:49 +03:00
} ;
2019-11-07 23:55:13 +02:00
std : : signal ( SIGINT , gotsig ) ;
std : : signal ( SIGTERM , gotsig ) ;
2019-04-27 12:48:48 +03:00
# ifdef Q_OS_UNIX
2019-11-07 23:55:13 +02:00
std : : signal ( SIGQUIT , gotsig ) ;
std : : signal ( SIGHUP , SIG_IGN ) ;
2019-04-23 02:31:49 +03:00
# endif
2019-04-26 06:23:49 +03:00
2019-11-18 08:29:33 +02:00
controller = std : : make_unique < Controller > ( options ) ;
2019-11-18 00:21:34 +02:00
controller - > startup ( ) ; // may throw
2019-11-12 22:14:04 +02:00
2019-11-18 00:21:34 +02:00
if ( ! options - > statsInterfaces . isEmpty ( ) ) {
2020-01-04 00:34:14 +02:00
const auto num = options - > statsInterfaces . count ( ) ;
2019-11-27 14:07:09 +02:00
Log ( ) < < " Stats HTTP: starting " < < num < < " " < < Util : : Pluralize ( " server " , num ) < < " ... " ;
2019-05-09 02:12:58 +03:00
// start 'stats' http servers, if any
2019-11-18 00:21:34 +02:00
for ( const auto & i : options - > statsInterfaces )
2019-05-09 02:12:58 +03:00
start_httpServer ( i ) ; // may throw
}
2019-04-23 02:31:49 +03:00
} catch ( const Exception & e ) {
2019-11-18 08:29:33 +02:00
Fatal ( ) < < " Caught exception: " < < e . what ( ) ;
2019-04-23 02:31:49 +03:00
}
}
void App : : cleanup ( )
{
Debug ( ) < < __PRETTY_FUNCTION__ ;
2019-12-29 00:45:09 +02:00
quitting = true ;
2019-12-21 13:24:53 +02:00
cleanup_WaitForThreadPoolWorkers ( ) ;
2019-05-09 02:12:58 +03:00
if ( ! httpServers . isEmpty ( ) ) {
Log ( " Stopping Stats HTTP Servers ... " ) ;
for ( auto h : httpServers ) { h - > stop ( ) ; }
httpServers . clear ( ) ; // deletes shared pointers
}
2019-11-18 00:21:34 +02:00
if ( controller ) { Log ( " Stopping Controller ... " ) ; controller - > cleanup ( ) ; controller . reset ( ) ; }
2019-04-27 14:55:10 +03:00
}
2019-12-21 13:24:53 +02:00
void App : : cleanup_WaitForThreadPoolWorkers ( )
2019-04-27 14:55:10 +03:00
{
2019-12-21 13:24:53 +02:00
constexpr int timeout = 5000 ;
2019-04-27 14:55:10 +03:00
QElapsedTimer t0 ; t0 . start ( ) ;
2019-12-21 13:24:53 +02:00
const int nJobs = Util : : ThreadPool : : ExtantJobs ( ) ;
if ( nJobs )
Log ( ) < < " Waiting for extant thread pool workers ... " ;
const bool res = Util : : ThreadPool : : ShutdownWaitForJobs ( timeout ) ;
if ( ! res ) {
Warning ( " After %d seconds, %d thread pool %s %s still active. App may abort with an error. " ,
qRound ( double ( t0 . elapsed ( ) ) / 1e3 ) , nJobs , Util : : Pluralize ( " worker " , nJobs ) . toUtf8 ( ) . constData ( ) ,
qAbs ( nJobs ) = = 1 ? " is " : " are " ) ;
} else if ( nJobs ) {
Debug ( " Successfully waited for %d thread pool %s (elapsed: %0.3f secs) " , nJobs ,
Util : : Pluralize ( " worker " , nJobs ) . toUtf8 ( ) . constData ( ) , t0 . elapsed ( ) / 1e3 ) ;
2019-04-27 12:38:50 +03:00
}
2019-04-23 02:31:49 +03:00
}
2019-04-23 23:26:07 +03:00
2019-04-27 14:55:10 +03:00
2019-04-23 23:26:07 +03:00
void App : : parseArgs ( )
{
QCommandLineParser parser ;
2019-11-04 00:41:47 +02:00
parser . setApplicationDescription ( " A Bitcoin Cash Blockchain SPV Server. " ) ;
2019-04-23 23:26:07 +03:00
parser . addHelpOption ( ) ;
parser . addVersionOption ( ) ;
2019-11-28 23:58:24 +02:00
static constexpr auto RPCUSER = " RPCUSER " , RPCPASSWORD = " RPCPASSWORD " ; // optional env vars we use below
2020-01-03 23:28:57 +02:00
QList < QCommandLineOption > allOptions {
2019-11-28 23:58:24 +02:00
{ { " D " , " datadir " } ,
QString ( " Specify a directory in which to store the database and other assorted data files. This is a "
2020-01-02 00:33:14 +02:00
" required option. If the specified path does not exist, it will be created. Note that the directory in "
2019-11-28 23:58:24 +02:00
" question should live on a fast drive such as an SSD and it should have plenty of free space available. " ) ,
2019-12-22 10:55:24 +02:00
QString ( " path " ) ,
2019-11-28 23:58:24 +02:00
} ,
2019-12-24 16:49:47 +02:00
{ { " t " , " tcp " } ,
QString ( " Specify an <interface:port> on which to listen for TCP connections, defaults to 0.0.0.0:%1 (all interfaces, "
" port %1 -- only if no other interfaces are specified via -t or -s). "
" This option may be specified more than once to bind to multiple interfaces and/or ports. " ) . arg ( Options : : DEFAULT_PORT_TCP ) ,
QString ( " interface:port " ) ,
} ,
{ { " s " , " ssl " } ,
QString ( " Specify an <interface:port> on which to listen for SSL connections. Note that if this option is "
" specified, then the `cert` and `key` options need to also be specified otherwise the app will refuse to run. "
" This option may be specified more than once to bind to multiple interfaces and/or ports. "
" Suggested value for port: %1. " ) . arg ( Options : : DEFAULT_PORT_SSL ) ,
2019-12-22 10:55:24 +02:00
QString ( " interface:port " ) ,
2019-11-28 23:58:24 +02:00
} ,
2019-12-24 16:49:47 +02:00
{ { " c " , " cert " } ,
QString ( " Specify a .crt file to use as the server's SSL cert. This option is required if the -s/--ssl option "
" appears at all on the command-line. The file should contain a valid non-self-signed certificate in PEM format. " ) ,
QString ( " crtfile " ) ,
} ,
{ { " k " , " key " } ,
QString ( " Specify a .key file to use as the server's SSL key. This option is required if the -s/--ssl option "
" appears at all on the command-line. The file should contain an RSA private key in PEM format. " ) ,
QString ( " keyfile " ) ,
} ,
2019-11-28 23:58:24 +02:00
{ { " z " , " stats " } ,
2020-01-03 23:28:57 +02:00
QString ( " Specify listen address and port for the stats HTTP server. Format is same as the -s or -t options, "
2019-11-28 23:58:24 +02:00
" e.g.: <interface:port>. Default is to not start any starts HTTP servers. "
" This option may be specified more than once to bind to multiple interfaces and/or ports. " ) ,
2019-12-22 10:55:24 +02:00
QString ( " interface:port " ) ,
2019-11-28 23:58:24 +02:00
} ,
{ { " b " , " bitcoind " } ,
QString ( " Specify a <hostname:port> 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=. " ) ,
2019-12-22 10:55:24 +02:00
QString ( " interface:port " ) ,
2019-11-28 23:58:24 +02:00
} ,
{ { " u " , " rpcuser " } ,
QString ( " Specify a username to use for authenticating to bitcoind. This is a required option, along "
2020-01-03 23:28:57 +02:00
" with -b and -p. This option should be the same username you specified in your bitcoind.conf file "
2019-11-28 23:58:24 +02:00
" under rpcuser=. For security, you may omit this option from the command-line and use the %1 "
" environment variable instead (the CLI arg takes precedence if both are present). " ) . arg ( RPCUSER ) ,
2019-12-22 10:55:24 +02:00
QString ( " username " ) ,
2019-11-28 23:58:24 +02:00
} ,
{ { " p " , " rpcpassword " } ,
QString ( " Specify a password to use for authenticating to bitcoind. This is a required option, along "
2020-01-03 23:28:57 +02:00
" with -b and -u. This option should be the same password you specified in your bitcoind.conf file "
2019-11-28 23:58:24 +02:00
" under rpcpassword=. For security, you may omit this option from the command-line and use the "
" %1 environment variable instead (the CLI arg takes precedence if both are present). " ) . arg ( RPCPASSWORD ) ,
2019-12-22 10:55:24 +02:00
QString ( " password " ) ,
2019-11-28 23:58:24 +02:00
} ,
{ { " d " , " debug " } ,
QString ( " Print extra verbose debug output. This is the default on debug builds. This is the opposite of -q. "
2019-12-22 10:55:24 +02:00
" (Specify this options twice to get network-level trace debug output.) " ) ,
2019-11-28 23:58:24 +02:00
} ,
{ { " q " , " quiet " } ,
2019-12-22 10:55:24 +02:00
QString ( " Suppress debug output. This is the default on release builds. This is the opposite of -d. " ) ,
2019-11-28 23:58:24 +02:00
} ,
{ { " S " , " syslog " } ,
2019-12-22 10:55:24 +02:00
QString ( " Syslog mode. If on Unix, use the syslog() facility to produce log messages. This option currently has no effect on Windows. " ) ,
} ,
{ { " C " , " checkdb " } ,
QString ( " If specified, database consistency will be checked thoroughly for sanity & integrity. "
" Note that these checks are somewhat slow to perform and under normal operation are not necessary. " ) ,
2019-11-28 23:58:24 +02:00
} ,
2019-12-28 23:26:16 +02:00
{ { " T " , " polltime " } ,
2020-01-03 23:28:57 +02:00
QString ( " The number of seconds for the bitcoind poll interval. Bitcoind is polled once every `polltime` "
" seconds to detect mempool and blockchain changes. This value must be at least 0.5 and cannot exceed "
2019-12-28 23:26:16 +02:00
" 30. If not specified, defaults to %1 seconds. " ) . arg ( Options : : defaultPollTimeSecs ) ,
2020-01-03 23:28:57 +02:00
QString ( " polltime " ) , QString : : number ( Options : : defaultPollTimeSecs )
2019-12-28 23:26:16 +02:00
} ,
2020-01-03 23:28:57 +02:00
} ;
parser . addOptions ( allOptions ) ;
parser . addPositionalArgument ( " config " , " Configuration file (optional). " , " [config] " ) ;
2019-04-23 23:26:07 +03:00
parser . process ( * this ) ;
2020-01-03 23:28:57 +02:00
ConfigFile conf ;
// First, parse config file (if specified) -- We will take whatever it specified that matches the above options
// but CLI args take precedence over config file options.
if ( auto posArgs = parser . positionalArguments ( ) ; ! posArgs . isEmpty ( ) ) {
if ( posArgs . size ( ) > 1 )
throw BadArgs ( " More than 1 config file was specified. Please specify at most 1 config file. " ) ;
const auto file = posArgs . first ( ) ;
if ( ! conf . open ( file ) )
throw BadArgs ( QString ( " Unable to open config file %1 " ) . arg ( file ) ) ;
// ok, at this point the config file is slurped up and we can check it below
}
// first warn user about dupes
for ( const auto & opt : allOptions ) {
static const auto DupeMsg = [ ] ( const QString & arg ) {
Log ( ) < < " ' " < < arg < < " ' specified both via the CLI and the configuration file. The CLI arg will take precedence. " ;
} ;
for ( const auto & name : opt . names ( ) ) {
if ( name . length ( ) = = 1 ) continue ;
if ( conf . hasValue ( name ) & & parser . isSet ( name ) ) {
DupeMsg ( name ) ;
conf . remove ( name ) ;
}
}
}
if ( parser . isSet ( " d " ) | | conf . boolValue ( " debug " ) ) {
//if (config.hasValue("debug"))
options - > verboseDebug = true ;
}
2019-11-11 00:22:31 +02:00
// check for -d -d
if ( auto found = parser . optionNames ( ) ; found . count ( " d " ) + found . count ( " debug " ) > 1 )
2019-11-18 00:21:34 +02:00
options - > verboseTrace = true ;
2020-01-04 00:34:14 +02:00
else {
// check for multiple debug = true in configFile (only present if no -d on CLI, otherwise config keys are deleted)
const auto l = conf . values ( " debug " ) ;
int ctr = 0 ;
for ( const auto & str : l )
ctr + = ( str . toInt ( ) | | QStringList { { " yes " , " true " , " on " , " " } } . contains ( str . toLower ( ) ) ) ? 1 : 0 ;
if ( ctr > 1 )
options - > verboseTrace = true ;
}
2020-01-03 23:28:57 +02:00
if ( parser . isSet ( " q " ) | | conf . boolValue ( " quiet " ) ) options - > verboseDebug = false ;
if ( parser . isSet ( " S " ) | | conf . boolValue ( " syslog " ) ) options - > syslogMode = true ;
if ( parser . isSet ( " C " ) | | conf . boolValue ( " checkdb " ) ) options - > doSlowDbChecks = true ;
// parse --polltime
// note despite how confusingly the below line reads, the CLI parser value takes precedence over the conf file here.
const QString polltimeStr = conf . value ( " polltime " , parser . value ( " T " ) ) ;
if ( bool ok ; ( options - > pollTimeSecs = polltimeStr . toDouble ( & ok ) ) < options - > minPollTimeSecs
2019-12-28 23:26:16 +02:00
| | ! ok | | options - > pollTimeSecs > options - > maxPollTimeSecs ) {
2020-01-03 23:28:57 +02:00
throw BadArgs ( QString ( " The 'polltime' option must be a numeric value in the range [%1, %2] " ) . arg ( options - > minPollTimeSecs ) . arg ( options - > maxPollTimeSecs ) ) ;
2019-12-28 23:26:16 +02:00
}
2019-11-07 23:55:13 +02:00
// make sure -b -p and -u all present and specified exactly once
2019-11-28 23:58:24 +02:00
using ReqOptsList = std : : list < std : : tuple < QString , QString , const char * > > ;
for ( const auto & opt : ReqOptsList ( { { " D " , " datadir " , nullptr } ,
{ " b " , " bitcoind " , nullptr } ,
{ " u " , " rpcuser " , RPCUSER } ,
{ " p " , " rpcpassword " , RPCPASSWORD } , } ) )
{
const auto & [ s , l , env ] = opt ;
const bool cliIsSet = parser . isSet ( s ) ;
2020-01-03 23:28:57 +02:00
const bool confIsSet = conf . hasValue ( l ) ;
2019-11-28 23:58:24 +02:00
const auto envVar = env ? std : : getenv ( env ) : nullptr ;
2020-01-03 23:28:57 +02:00
if ( ( cliIsSet | | confIsSet ) & & envVar )
2020-01-04 00:43:52 +02:00
Warning ( ) < < " Warning: " < < l < < " is specified both via the " < < ( cliIsSet ? " CLI " : " config file " )
2020-01-03 23:28:57 +02:00
< < " and the environement (as " < < env < < " ). The " < < ( cliIsSet ? " CLI arg " : " config file setting " )
< < " will take precendence. " ;
if ( ( ( ! cliIsSet & & ! confIsSet ) | | conf . value ( l , parser . value ( s ) ) . isEmpty ( ) ) & & ( ! env | | ! envVar ) )
2019-11-28 23:58:24 +02:00
throw BadArgs ( QString ( " Required option missing or empty: -%1 (--%2) % 3 " ).arg(s).arg(l).arg(env ? QString( " ( or env var : % 1 ) " ).arg(env) : " " )) ;
else if ( parser . values ( s ) . count ( ) > 1 )
2019-11-07 23:55:13 +02:00
throw BadArgs ( QString ( " Option specified multiple times: -%1 (--%2) " ).arg(s).arg(l)) ;
2020-01-04 00:34:14 +02:00
else if ( conf . values ( l ) . count ( ) > 1 )
throw BadArgs ( QString ( " This option cannot be specified multiple times in the config file: %1 " ) . arg ( l ) ) ;
2019-11-07 23:55:13 +02:00
}
static const auto parseInterface = [ ] ( const QString & s ) - > Options : : Interface {
auto toks = s . split ( " : " ) ;
if ( toks . length ( ) < 2 )
throw BadArgs ( " Malformed interface spec. Please pass a address of the form 1.2.3.4:123 for IPv4 or ::1:123 for IPv6. " ) ;
QString portStr = toks . last ( ) ;
toks . removeLast ( ) ;
QString hostStr = toks . join ( " : " ) ;
QHostAddress h ( hostStr ) ;
if ( h . isNull ( ) )
throw BadArgs ( QString ( " Bad interface address: %1 " ) . arg ( hostStr ) ) ;
bool ok ;
quint16 port = portStr . toUShort ( & ok ) ;
if ( ! ok | | port = = 0 )
throw BadArgs ( QString ( " Bad port: %1 " ) . arg ( portStr ) ) ;
return { h , port } ;
} ;
2019-05-09 02:12:58 +03:00
static const auto parseInterfaces = [ ] ( decltype ( Options : : interfaces ) & interfaces , const QStringList & l ) {
// functor parses -i and -z options, puts results in 'interfaces' passed-in reference.
interfaces . clear ( ) ;
2019-11-07 23:55:13 +02:00
for ( const auto & s : l )
interfaces . push_back ( parseInterface ( s ) ) ;
2019-05-09 02:12:58 +03:00
} ;
2019-11-28 23:58:24 +02:00
// grab datadir, check it's good, create it if needed
2020-01-03 23:28:57 +02:00
options - > datadir = conf . value ( " datadir " , parser . value ( " D " ) ) ;
2019-11-28 23:58:24 +02:00
QFileInfo fi ( options - > datadir ) ;
if ( auto path = fi . canonicalFilePath ( ) ; fi . exists ( ) ) {
if ( ! fi . isDir ( ) ) // was a file and not a directory
throw BadArgs ( QString ( " The specified path \" %1 \" already exists but is not a directory " ) . arg ( path ) ) ;
if ( ! fi . isReadable ( ) | | ! fi . isExecutable ( ) | | ! fi . isWritable ( ) )
throw BadArgs ( QString ( " Bad permissions for path \" %1 \" (must be readable, writable, and executable) " ).arg(path)) ;
2020-01-03 23:28:57 +02:00
Util : : AsyncOnObject ( this , [ path ] { Debug ( ) < < " datadir: " < < path ; } ) ; // log this after return to event loop so it ends up in syslog (if -S mode)
2019-11-28 23:58:24 +02:00
} else { // !exists
if ( ! QDir ( ) . mkpath ( options - > datadir ) )
throw BadArgs ( QString ( " Unable to create directory: %1 " ) . arg ( options - > datadir ) ) ;
path = QFileInfo ( options - > datadir ) . canonicalFilePath ( ) ;
2020-01-04 00:49:15 +02:00
// log this after return to event loop so it ends up in syslog (in case user specified -S mode)
Util : : AsyncOnObject ( this , [ path ] { Debug ( ) < < " datadir: Created directory " < < path ; } ) ;
2019-11-28 23:58:24 +02:00
}
2020-01-03 23:28:57 +02:00
// parse bitcoind - conf.value is always unset if parser.value is set, hence this strange constrcution below (parser.value takes precedence)
options - > bitcoind = parseInterface ( conf . value ( " bitcoind " , parser . value ( " b " ) ) ) ;
2019-11-07 23:55:13 +02:00
// grab rpcuser
2020-01-03 23:28:57 +02:00
options - > rpcuser = conf . value ( " rpcuser " , parser . isSet ( " u " ) ? parser . value ( " u " ) : std : : getenv ( RPCUSER ) ) ;
2019-11-07 23:55:13 +02:00
// grab rpcpass
2020-01-03 23:28:57 +02:00
options - > rpcpassword = conf . value ( " rpcpassword " , parser . isSet ( " p " ) ? parser . value ( " p " ) : std : : getenv ( RPCPASSWORD ) ) ;
2019-12-24 16:49:47 +02:00
bool tcpIsDefault = true ;
2020-01-03 23:28:57 +02:00
// grab bind (listen) interfaces for TCP -- this hard-to-read code here looks at both conf.value and parser, but conf.value only has values if parser does not (CLI parser takes precedence).
2020-01-04 00:34:14 +02:00
if ( auto l = conf . hasValue ( " tcp " ) ? conf . values ( " tcp " ) : parser . values ( " t " ) ; ! l . isEmpty ( ) ) {
2019-11-18 00:21:34 +02:00
parseInterfaces ( options - > interfaces , l ) ;
2019-12-24 16:49:47 +02:00
tcpIsDefault = false ;
2020-01-04 22:32:05 +02:00
if ( ! options - > interfaces . isEmpty ( ) )
// save default publicTcp we will report now -- note this may get reset() to !has_value() later in
// this function if user explicitly specified public_tcp_port=0 in the config file.
options - > publicTcp = options - > interfaces . front ( ) . second ;
2019-12-24 16:49:47 +02:00
}
2020-01-03 23:28:57 +02:00
// grab bind (listen) interfaces for SSL (again, apologies for this hard to read expression below -- same comments as above apply here)
2020-01-04 00:34:14 +02:00
if ( auto l = conf . hasValue ( " ssl " ) ? conf . values ( " ssl " ) : parser . values ( " s " ) ; ! l . isEmpty ( ) ) {
2019-12-24 16:49:47 +02:00
if ( ! QSslSocket : : supportsSsl ( ) ) {
throw InternalError ( " SSL support is not compiled and/or linked to this version. Cannot proceed with SSL support. Sorry! " ) ;
}
parseInterfaces ( options - > sslInterfaces , l ) ;
if ( tcpIsDefault ) options - > interfaces . clear ( ) ; // they had default tcp setup, clear the default since they did end up specifying at least 1 real interface to bind to
2020-01-04 22:32:05 +02:00
if ( ! options - > sslInterfaces . isEmpty ( ) )
// save default publicSsl we will report now -- note this may get reset() to !has_value() later in
// this function if user explicitly specified public_ssl_port=0 in the config file.
options - > publicSsl = options - > sslInterfaces . front ( ) . second ;
2019-12-24 16:49:47 +02:00
}
2019-12-24 17:10:01 +02:00
if ( ! options - > sslInterfaces . isEmpty ( ) ) {
2020-01-03 23:28:57 +02:00
const QString cert = conf . value ( " cert " , parser . value ( " c " ) ) , key = conf . value ( " key " , parser . value ( " k " ) ) ;
2019-12-24 17:10:01 +02:00
if ( cert . isEmpty ( ) | | key . isEmpty ( ) ) {
throw BadArgs ( " SSL option requires both -c/--cert and -k/--key options be specified on the command-line " ) ;
} else if ( ! QFile : : exists ( cert ) ) {
throw BadArgs ( QString ( " Cert file not found: %1 " ) . arg ( cert ) ) ;
} else if ( ! QFile : : exists ( key ) ) {
throw BadArgs ( QString ( " Key file not found: %1 " ) . arg ( key ) ) ;
} else {
QFile certf ( cert ) , keyf ( key ) ;
if ( ! certf . open ( QIODevice : : ReadOnly ) )
throw BadArgs ( QString ( " Unable to open cert file %1: %2 " ) . arg ( cert ) . arg ( certf . errorString ( ) ) ) ;
if ( ! keyf . open ( QIODevice : : ReadOnly ) )
throw BadArgs ( QString ( " Unable to open key file %1: %2 " ) . arg ( key ) . arg ( keyf . errorString ( ) ) ) ;
options - > sslCert = QSslCertificate ( & certf , QSsl : : EncodingFormat : : Pem ) ;
options - > sslKey = QSslKey ( & keyf , QSsl : : KeyAlgorithm : : Rsa , QSsl : : EncodingFormat : : Pem ) ;
if ( options - > sslCert . isNull ( ) )
throw BadArgs ( QString ( " Unable to read ssl certificate from %1. Please make sure the file is readable and "
" contains a valid certificate in PEM format. " ) . arg ( cert ) ) ;
2019-12-31 19:29:48 +02:00
else {
2020-01-04 00:49:15 +02:00
Util : : AsyncOnObject ( this , [ this ] {
// We do this logging later. This is to ensure that it ends up in the syslog if user specified -S
QString name ;
2019-12-31 19:29:48 +02:00
# if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
2020-01-04 00:49:15 +02:00
// Was added Qt 5.12+
name = options - > sslCert . subjectDisplayName ( ) ;
2019-12-31 19:29:48 +02:00
# else
2020-01-04 00:49:15 +02:00
name = options - > sslCert . subjectInfo ( QSslCertificate : : Organization ) . join ( " , " ) ;
2019-12-31 19:29:48 +02:00
# endif
2020-01-03 23:28:57 +02:00
Log ( ) < < " Loaded SSL certificate: " < < name < < " "
< < options - > sslCert . subjectInfo ( QSslCertificate : : SubjectInfo : : EmailAddress ) . join ( " , " )
//<< " self-signed: " << (options->sslCert.isSelfSigned() ? "YES" : "NO")
< < " expires: " < < ( options - > sslCert . expiryDate ( ) . toString ( " ddd MMMM d yyyy hh:mm:ss " ) ) ;
} ) ;
2019-12-31 19:29:48 +02:00
}
2019-12-24 17:10:01 +02:00
if ( options - > sslKey . isNull ( ) )
throw BadArgs ( QString ( " Unable to read private key from %1. Please make sure the file is readable and "
" contains a single RSA private key in PEM format. " ) . arg ( key ) ) ;
2020-01-03 23:28:57 +02:00
static const auto KeyAlgoStr = [ ] ( QSsl : : KeyAlgorithm a ) {
2019-12-24 16:49:47 +02:00
switch ( a ) {
2020-01-02 01:03:24 +02:00
# if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
// This was added in Qt 5.13+
2019-12-24 16:49:47 +02:00
case QSsl : : KeyAlgorithm : : Dh : return " DH " ;
2019-12-31 19:29:48 +02:00
# endif
2019-12-24 16:49:47 +02:00
case QSsl : : KeyAlgorithm : : Ec : return " EC " ;
case QSsl : : KeyAlgorithm : : Dsa : return " DSA " ;
case QSsl : : KeyAlgorithm : : Rsa : return " RSA " ;
default : return " Other " ;
}
} ;
2020-01-04 00:49:15 +02:00
Util : : AsyncOnObject ( this , [ this ] {
2020-01-03 23:28:57 +02:00
// We do this logging later. This is to ensure that it ends up in the syslog if user specified -S
Log ( ) < < " Loaded key type: " < < ( options - > sslKey . type ( ) = = QSsl : : KeyType : : PrivateKey ? " private " : " public " )
< < " algorithm: " < < KeyAlgoStr ( options - > sslKey . algorithm ( ) ) ;
} ) ;
2019-12-24 16:49:47 +02:00
}
2019-04-23 23:26:07 +03:00
}
2020-01-03 23:28:57 +02:00
parseInterfaces ( options - > statsInterfaces , conf . hasValue ( " stats " )
2020-01-04 00:34:14 +02:00
? conf . values ( " stats " )
2020-01-03 23:28:57 +02:00
: parser . values ( " z " ) ) ;
2020-01-04 12:11:17 +02:00
/// misc conf-only variables ...
options - > donationAddress = conf . value ( " donation " , options - > donationAddress ) . left ( 80 ) ; // the 80 character limit is in case the user specified a crazy long string, no need to send all of it -- it's probably invalid anyway.
options - > bannerFile = conf . value ( " banner " , options - > bannerFile ) ;
2020-01-04 22:32:05 +02:00
if ( conf . hasValue ( " hostname " ) )
options - > hostName = conf . value ( " hostname " ) ;
if ( conf . hasValue ( " public_tcp_port " ) ) {
bool ok ;
int val = conf . intValue ( " public_tcp_port " , - 1 , & ok ) ;
if ( ! ok | | val < 0 | | val > UINT16_MAX )
throw BadArgs ( " public_tcp_port parse error: not an integer from 0 to 65535 " ) ;
if ( ! val ) options - > publicTcp . reset ( ) ;
else options - > publicTcp = val ;
}
if ( conf . hasValue ( " public_ssl_port " ) ) {
bool ok ;
int val = conf . intValue ( " public_ssl_port " , - 1 , & ok ) ;
if ( ! ok | | val < 0 | | val > UINT16_MAX )
throw BadArgs ( " public_ssl_port parse error: not an integer from 0 to 65535 " ) ;
if ( ! val ) options - > publicSsl . reset ( ) ;
else options - > publicSsl = val ;
}
// warn user that no hostname was specified if they have peerDiscover turned on
if ( ! options - > hostName . has_value ( ) & & options - > peerDiscovery & & options - > peerAnnounceSelf ) {
// do this when we return to event loop in case user is logging to -S (so it appears in syslog which gets set up after we return)
Util : : AsyncOnObject ( this , [ ] {
Warning ( ) < < " Warning: No 'hostname' variable defined in configuration. This server may not be peer-discoverable. " ;
} ) ;
}
2019-04-23 23:26:07 +03:00
}
2019-05-09 02:12:58 +03:00
2019-12-12 12:07:18 +02:00
namespace {
auto ParseParams ( const SimpleHttpServer : : Request & req ) - > StatsMixin : : StatsParams {
StatsMixin : : StatsParams params ;
const auto nvps = req . queryString . split ( " & " ) ;
for ( const auto & nvp : nvps ) {
auto nv = nvp . split ( " = " ) ;
if ( nv . size ( ) = = 2 )
params [ nv . front ( ) ] = nv . back ( ) ;
}
return params ;
}
}
2019-05-09 02:12:58 +03:00
void App : : start_httpServer ( const Options : : Interface & iface )
{
2019-11-07 23:55:13 +02:00
std : : shared_ptr < SimpleHttpServer > server ( new SimpleHttpServer ( iface . first , iface . second , 16384 ) ) ;
2019-05-09 02:12:58 +03:00
httpServers . push_back ( server ) ;
server - > tryStart ( ) ; // may throw, waits for server to start
2019-12-12 12:07:18 +02:00
server - > set404Message ( " Error: Unknown endpoint. /stats & /debug are the only valid endpoint I understand. \r \n " ) ;
2019-05-09 02:12:58 +03:00
server - > addEndpoint ( " /stats " , [ this ] ( SimpleHttpServer : : Request & req ) {
req . response . contentType = " application/json; charset=utf-8 " ;
2019-12-13 00:14:09 +02:00
auto stats = controller - > statsSafe ( ) ;
2019-12-12 12:07:18 +02:00
stats = stats . isNull ( ) ? QVariantList { QVariant ( ) } : stats ;
req . response . data = QString ( " %1 \r \n " ) . arg ( Util : : Json : : toString ( stats , false ) ) . toUtf8 ( ) ;
} ) ;
server - > addEndpoint ( " /debug " , [ this ] ( SimpleHttpServer : : Request & req ) {
req . response . contentType = " application/json; charset=utf-8 " ;
const auto params = ParseParams ( req ) ;
2019-12-13 00:14:09 +02:00
auto stats = controller - > debugSafe ( params ) ;
2019-12-12 12:07:18 +02:00
stats = stats . isNull ( ) ? QVariantList { QVariant ( ) } : stats ;
2019-05-09 02:12:58 +03:00
req . response . data = QString ( " %1 \r \n " ) . arg ( Util : : Json : : toString ( stats , false ) ) . toUtf8 ( ) ;
} ) ;
}
2019-12-24 16:49:47 +02:00
void App : : miscPreAppFixups ( )
{
# ifdef Q_OS_DARWIN
// workaround for annoying macos keychain access prompt. see: https://doc.qt.io/qt-5/qsslsocket.html#setLocalCertificate
setenv ( " QT_SSL_USE_TEMPORARY_KEYCHAIN " , " 1 " , 1 ) ;
# endif
}