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-11-08 17:11:58 +02:00
# include "Servers.h"
2019-12-20 19:12:05 +02:00
# include "BitcoinD.h"
# include "Merkle.h"
2019-12-18 11:58:25 +02:00
# include "Storage.h"
2019-12-29 22:57:04 +02:00
# include "SubsMgr.h"
2019-12-18 11:58:25 +02:00
# include <QByteArray>
2019-04-26 08:47:46 +03:00
# include <QCoreApplication>
# include <QtNetwork>
2019-05-09 02:12:58 +03:00
# include <QString>
# include <QTextCodec>
2019-12-18 11:58:25 +02:00
# include <QTextStream>
2019-11-08 02:49:09 +02:00
# include <QTimer>
2019-04-26 08:47:46 +03:00
2019-11-08 16:34:20 +02:00
# include <cstdlib>
2019-12-18 11:58:25 +02:00
# include <utility>
2019-11-08 16:34:20 +02:00
2019-04-26 08:47:46 +03:00
TcpServerError : : ~ TcpServerError ( ) { } // for vtable
2019-05-09 02:12:58 +03:00
AbstractTcpServer : : AbstractTcpServer ( const QHostAddress & a , quint16 p )
2019-04-28 11:06:50 +03:00
: QTcpServer ( nullptr ) , IdMixin ( newId ( ) ) , addr ( a ) , port ( p )
2019-04-26 08:47:46 +03:00
{
2019-11-17 10:59:50 +02:00
assert ( qobj ( ) ) ; // Runtime check that derived class followed the rules outlined at the top of Mixins.h
2019-04-26 08:47:46 +03:00
_thread . setObjectName ( prettyName ( ) ) ;
2019-04-28 11:06:50 +03:00
setObjectName ( prettyName ( ) ) ;
2019-04-26 08:47:46 +03:00
}
2019-05-09 02:12:58 +03:00
AbstractTcpServer : : ~ AbstractTcpServer ( )
2019-04-26 08:47:46 +03:00
{
Debug ( ) < < __FUNCTION__ ;
stop ( ) ;
}
2019-05-09 02:12:58 +03:00
QString AbstractTcpServer : : hostPort ( ) const
2019-04-26 08:47:46 +03:00
{
return QString ( " %1:%2 " ) . arg ( addr . toString ( ) ) . arg ( port ) ;
}
2019-05-09 02:12:58 +03:00
QString AbstractTcpServer : : prettyName ( ) const
2019-04-26 08:47:46 +03:00
{
2019-04-28 11:06:50 +03:00
return QString ( " Srv %1 (id: %2) " ).arg(hostPort()).arg(id) ;
2019-04-26 08:47:46 +03:00
}
2019-11-12 11:29:45 +02:00
void AbstractTcpServer : : tryStart ( ulong timeout_ms )
2019-04-26 08:47:46 +03:00
{
if ( ! _thread . isRunning ( ) ) {
2019-04-28 11:59:14 +03:00
ThreadObjectMixin : : start ( ) ; // call super
2019-04-28 11:06:50 +03:00
Log ( ) < < " Starting listener service for " < < prettyName ( ) < < " ... " ;
2019-11-12 11:29:45 +02:00
if ( auto result = chan . get < QString > ( timeout_ms ) ; result ! = " ok " ) {
2019-04-26 08:47:46 +03:00
result = result . isEmpty ( ) ? " Startup timed out! " : result ;
throw TcpServerError ( result ) ;
}
Log ( ) < < " Service started, listening for connections on " < < hostPort ( ) ;
} else {
throw TcpServerError ( prettyName ( ) + " already started " ) ;
}
}
2019-05-09 02:12:58 +03:00
void AbstractTcpServer : : on_started ( )
2019-04-26 08:47:46 +03:00
{
QString result = " ok " ;
2019-05-09 02:12:58 +03:00
conns . push_back ( connect ( this , SIGNAL ( newConnection ( ) ) , this , SLOT ( pvt_on_newConnection ( ) ) ) ) ;
conns . push_back ( connect ( this , & QTcpServer : : acceptError , this , [ this ] ( QAbstractSocket : : SocketError e ) { on_acceptError ( e ) ; } ) ) ;
2019-04-26 08:47:46 +03:00
if ( ! listen ( addr , port ) ) {
result = errorString ( ) ;
2019-11-18 08:29:33 +02:00
result = result . isEmpty ( ) ? " Error binding/listening for connections " : QString ( " Could not bind to %1: %2 " ) . arg ( hostPort ( ) ) . arg ( result ) ;
2019-04-26 08:47:46 +03:00
Debug ( ) < < __FUNCTION__ < < " listen failed " ;
} else {
Debug ( ) < < " started ok " ;
}
chan . put ( result ) ;
}
2019-05-09 02:12:58 +03:00
void AbstractTcpServer : : on_finished ( )
2019-04-26 08:47:46 +03:00
{
close ( ) ; /// stop listening
chan . put ( " finished " ) ;
2019-05-04 06:00:01 +03:00
Debug ( ) < < objectName ( ) < < " finished. " ;
2019-04-28 11:59:14 +03:00
ThreadObjectMixin : : on_finished ( ) ;
2019-04-26 08:47:46 +03:00
}
2019-05-09 02:12:58 +03:00
void AbstractTcpServer : : on_acceptError ( QAbstractSocket : : SocketError e )
{
Error ( ) < < objectName ( ) < < " ; error acceptError, code: " < < int ( e ) ;
}
/*static*/
QString AbstractTcpServer : : prettySock ( QAbstractSocket * sock )
2019-04-26 08:47:46 +03:00
{
return QString ( " %1:%2 " )
. arg ( sock ? sock - > peerAddress ( ) . toString ( ) : " (null) " )
. arg ( sock ? sock - > peerPort ( ) : 0 ) ;
}
2019-05-09 02:12:58 +03:00
void AbstractTcpServer : : pvt_on_newConnection ( )
2019-04-26 08:47:46 +03:00
{
QTcpSocket * sock = nextPendingConnection ( ) ;
if ( sock ) {
Debug ( ) < < " Got connection from: " < < prettySock ( sock ) ;
2019-05-09 02:12:58 +03:00
on_newConnection ( sock ) ;
2019-04-26 08:47:46 +03:00
} else {
Warning ( ) < < __FUNCTION__ < < " : nextPendingConnection returned a nullptr! Called at the wrong time? FIXME! " ;
}
}
2019-04-27 22:43:09 +03:00
2019-05-09 02:12:58 +03:00
SimpleHttpServer : : SimpleHttpServer ( const QHostAddress & listenAddr , quint16 listenPort , qint64 maxBuffer , qint64 timeLimit )
: AbstractTcpServer ( listenAddr , listenPort ) , MAX_BUFFER ( maxBuffer > 0 ? maxBuffer : DEFAULT_MAX_BUFFER ) ,
TIME_LIMIT ( timeLimit )
2019-05-04 09:37:42 +03:00
{
2019-05-09 02:12:58 +03:00
// re-set name for debug/logging
_thread . setObjectName ( prettyName ( ) ) ;
setObjectName ( prettyName ( ) ) ;
}
QString SimpleHttpServer : : prettyName ( ) const
{
return QString ( " Http%1 " ) . arg ( AbstractTcpServer : : prettyName ( ) ) ;
}
void SimpleHttpServer : : on_newConnection ( QTcpSocket * sock )
{
sock - > setReadBufferSize ( MAX_BUFFER ) ;
const QString sockName ( prettySock ( sock ) ) ;
connect ( sock , & QAbstractSocket : : disconnected , this , [ sock , sockName ] {
Debug ( ) < < sockName < < " disconnected " ;
sock - > deleteLater ( ) ;
} ) ;
connect ( sock , & QObject : : destroyed , this , [ sockName ] ( QObject * ) {
Debug ( ) < < sockName < < " destroyed " ;
} ) ;
connect ( sock , & QAbstractSocket : : readyRead , this , [ sock , sockName , this ] {
try {
while ( sock - > canReadLine ( ) ) {
auto line = QString ( sock - > readLine ( ) ) . trimmed ( ) ;
//Debug() << sockName << " Got line: " << line;
if ( QString loc = sock - > property ( " req-loc " ) . toString ( ) ; loc . isEmpty ( ) ) {
auto toks = line . split ( " " ) ;
if ( toks . length ( ) ! = 3 | | ( toks [ 0 ] ! = " GET " & & toks [ 1 ] ! = " POST " ) | | toks [ 2 ] ! = " HTTP/1.1 " )
throw Exception ( QString ( " Invalid request: %1 " ) . arg ( line ) ) ;
2019-11-11 00:22:31 +02:00
Trace ( ) < < sockName < < " " < < line ;
2019-05-09 02:12:58 +03:00
sock - > setProperty ( " req-loc " , toks [ 1 ] ) ;
sock - > setProperty ( " req-meth " , toks [ 0 ] ) ;
sock - > setProperty ( " req-ver " , toks [ 2 ] ) ;
} else if ( auto loc = sock - > property ( " req-loc " ) . toString ( ) ,
meth = sock - > property ( " req-meth " ) . toString ( ) ,
ver = sock - > property ( " req-ver " ) . toString ( ) ;
line . isEmpty ( ) & & ! loc . isEmpty ( ) & & ! meth . isEmpty ( ) & & ! ver . isEmpty ( ) ) {
// got line by itself, prepare response
Request req ;
auto & response = req . response ;
req . httpVersion = ver ;
req . method = meth = = " GET " ? Method : : GET : Method : : POST ;
auto vmap = sock - > property ( " req-header " ) . toMap ( ) ;
for ( auto it = vmap . begin ( ) ; it ! = vmap . end ( ) ; + + it )
// save header
req . header [ it . key ( ) ] = it . value ( ) . toString ( ) ;
if ( auto i = loc . indexOf ( ' ? ' ) ; i > - 1 ) {
req . queryString = loc . mid ( i + 1 ) ;
req . endPoint = loc . left ( i ) ;
} else
req . endPoint = loc ;
if ( auto it = endPoints . find ( req . endPoint ) ; it ! = endPoints . end ( ) | | ( it = endPoints . find ( " * " ) ) ! = endPoints . end ( ) ) {
it . value ( ) ( req ) ; // call lambda
} else {
// could not find any enpoints that match, set up a 404 response
response . status = 404 ;
response . statusText = " Unknown resource " ;
response . data = err404Msg . toUtf8 ( ) ;
}
// setup header
QByteArray responseHeader ;
{
QTextStream ss ( & responseHeader , QIODevice : : WriteOnly ) ;
ss . setCodec ( QTextCodec : : codecForName ( " UTF-8 " ) ) ;
ss < < " HTTP/1.1 " < < response . status < < " " < < req . response . statusText . trimmed ( ) < < " \r \n " ;
ss < < " Content-Type: " < < response . contentType . trimmed ( ) < < " \r \n " ;
ss < < " Content-Length: " < < response . data . length ( ) < < " \r \n " ;
ss < < response . headerExtra ;
ss < < " \r \n " ;
}
const qint64 respTotalLen = responseHeader . length ( ) + response . data . length ( ) ;
sock - > setProperty ( " resp-len " , respTotalLen ) ;
// write out header
sock - > write ( responseHeader ) ;
if ( response . data . length ( ) )
// write out response data
sock - > write ( response . data ) ;
} else {
// save params
auto vmap = sock - > property ( " req-header " ) . toMap ( ) ;
auto toks = line . split ( " : " ) ;
if ( toks . length ( ) > = 2 ) {
auto name = toks . front ( ) ; toks . pop_front ( ) ;
auto value = toks . join ( " : " ) ;
vmap [ name ] = value ;
sock - > setProperty ( " req-header " , vmap ) ;
} else
throw Exception ( " garbage data, closing connection " ) ;
}
}
if ( sock - > bytesAvailable ( ) > MAX_BUFFER )
throw Exception ( " too much data, closing connection " ) ;
} catch ( const std : : exception & e ) {
Warning ( ) < < " Client: " < < sockName < < " ; " < < e . what ( ) ;
sock - > abort ( ) ;
sock - > deleteLater ( ) ;
}
} ) ;
connect ( sock , & QAbstractSocket : : bytesWritten , this , [ sock , sockName ] ( qint64 bytes ) {
qint64 nWrit = sock - > property ( " resp-written " ) . toLongLong ( ) + bytes ;
sock - > setProperty ( " resp-written " , nWrit ) ;
auto var = sock - > property ( " resp-len " ) ;
if ( const auto n2write = var . toLongLong ( ) ; ! var . isNull ( ) & & nWrit > = n2write ) {
// graceful disconnect
Debug ( ) < < sockName < < " wrote " < < nWrit < < " / " < < n2write < < " bytes, disconnecting " ;
sock - > disconnectFromHost ( ) ;
} else {
2019-11-11 00:22:31 +02:00
Trace ( ) < < sockName < < " wrote: " < < bytes < < " bytes " ;
2019-05-09 02:12:58 +03:00
}
} ) ;
if ( TIME_LIMIT > 0 ) {
QTimer : : singleShot ( TIME_LIMIT , sock , [ sock , sockName , this ] {
Debug ( ) < < sockName < < " killing connection after " < < ( TIME_LIMIT / 1e3 ) < < " seconds " ;
sock - > abort ( ) ;
sock - > deleteLater ( ) ;
} ) ;
}
}
void SimpleHttpServer : : addEndpoint ( const QString & endPoint , const Lambda & callback )
{
if ( ! endPoint . startsWith ( " / " ) & & endPoint ! = " * " )
Warning ( ) < < __FUNCTION__ < < " endPoint " < < endPoint < < " does not start with '/' -- it will never be reached! FIXME! " ;
endPoints [ endPoint ] = callback ;
}
2019-11-16 23:33:40 +02:00
// ---- Classes Server & Client ----
// class Server constants
namespace {
// TODO: maybe move these to a more global place? For now here is fine.
namespace Constants {
constexpr int kMaxServerVersion = 80 , ///< the maximum server version length we accept to prevent memory exhaustion attacks
kMaxBuffer = 4 * 1000 * 1000 , ///< =4MB. The max buffer we use in Client (ElectronX client). TODO: Make this tune-able and configurable!
2019-12-19 14:29:48 +02:00
kMaxTxHex = 2 * 1024 * 1024 , ///< >1MB raw tx max (over 1 MiB, 1 traditional PoT MB should be enough).
2019-11-16 23:33:40 +02:00
kMaxErrorCount = 10 ; ///< The maximum number of errors we tolerate from a Client before disconnecting them.
2019-12-19 14:29:48 +02:00
// types in a Message.params object that we accept as booleans
const std : : set < QVariant : : Type > acceptableBoolVariantTypes = {
QVariant : : Type : : Bool , QVariant : : Type : : Int , QVariant : : Type : : UInt , QVariant : : Type : : LongLong ,
QVariant : : Type : : ULongLong , QVariant : : Type : : Double ,
} ;
2019-11-16 23:33:40 +02:00
}
using namespace Constants ;
2019-12-19 14:29:48 +02:00
std : : pair < bool , bool > parseBoolSemiLooselyButNotTooLoosely ( const QVariant & v ) {
std : : pair < bool , bool > ret { false , false } ;
if ( acceptableBoolVariantTypes . count ( v . type ( ) ) )
ret = { v . toBool ( ) , true } ;
return ret ;
}
2019-12-19 16:56:26 +02:00
QString formatBitcoinDErrorResponseToLookLikeDumbElectrumXPythonRepr ( const RPC : : Message & errResponse ) {
constexpr auto escapeSingleQuote = [ ] ( const QString & s ) - > QString {
const QByteArray b = s . toUtf8 ( ) ;
QByteArray ret ;
ret . reserve ( int ( b . length ( ) * 1.5 ) ) ;
bool inesc = false ;
for ( int i = 0 ; i < b . size ( ) ; + + i ) {
const char c = b [ i ] ;
if ( c = = ' \\ ' )
inesc = ! inesc ;
else if ( c = = ' \' ' & & ! inesc )
ret . push_back ( ' \\ ' ) ;
else
inesc = false ;
ret . push_back ( c ) ;
}
return QString : : fromUtf8 ( ret ) ;
} ;
return QString ( " daemon error: DaemonError({'code': %1, 'message': '%2'}) " )
. arg ( errResponse . errorCode ( ) ) . arg ( escapeSingleQuote ( errResponse . errorMessage ( ) ) ) ;
}
/// used internally by RPC methods. Given a hashHex, ensure it's 32 bytes (or DataLen) of hash data and nothing else.
/// Returns DataLen (default=32) bytes of valid hex decoded data or an empty QByteArray on failure.
QByteArray validateHashHex ( const QString & hashHex , const int DataLen = HashLen ) {
QByteArray ret = hashHex . trimmed ( ) . left ( DataLen * 2 ) . toUtf8 ( ) ;
// ugh, QByteArray returns dummy bytes at the end if it can't fully parse. So we have to check the original
// hash byte length as well.
if ( ret . length ( ) ! = DataLen * 2 | | ( ret = QByteArray : : fromHex ( ret ) ) . length ( ) ! = DataLen )
ret . clear ( ) ;
return ret ;
}
2019-11-16 23:33:40 +02:00
}
2019-12-30 10:33:35 +02:00
Server : : Server ( const QHostAddress & a , quint16 p , const std : : shared_ptr < Storage > & s , const std : : shared_ptr < BitcoinDMgr > & bdm )
: AbstractTcpServer ( a , p ) , storage ( s ) , bitcoindmgr ( bdm )
2019-05-09 02:12:58 +03:00
{
// re-set name for debug/logging
_thread . setObjectName ( prettyName ( ) ) ;
setObjectName ( prettyName ( ) ) ;
2019-11-09 00:04:28 +02:00
StaticData : : init ( ) ; // only does something first time it's called, otherwise a no-op
2019-05-09 02:12:58 +03:00
}
2019-11-07 23:55:13 +02:00
Server : : ~ Server ( ) { stop ( ) ; } // paranoia about pure virtual, and vtable consistency, etc
2019-05-09 02:12:58 +03:00
2019-11-07 23:55:13 +02:00
QString Server : : prettyName ( ) const
2019-05-09 02:12:58 +03:00
{
return QString ( " Tcp%1 " ) . arg ( AbstractTcpServer : : prettyName ( ) ) ;
2019-05-04 09:37:42 +03:00
}
2019-11-09 21:14:07 +02:00
// this must be called in the thread context of this thread
QVariantMap Server : : stats ( ) const
{
QVariantMap ret ;
2019-11-16 22:27:21 +02:00
ret [ " numClients " ] = clientsById . count ( ) ;
QVariantList clientList ;
for ( const auto & client : clientsById ) {
2019-11-17 00:18:44 +02:00
// note we call this thread-unsafe function stats() here because client lives in our thread. but if that design
// changes, update this to call client->statsSafe(100) instead
2019-11-27 14:07:09 +02:00
auto map = client - > stats ( ) . toMap ( ) ;
2019-11-16 22:27:21 +02:00
auto name = map . take ( " name " ) . toString ( ) ;
2019-11-16 23:33:40 +02:00
map [ " version " ] = QVariantList ( { client - > info . userAgent , client - > info . protocolVersion } ) ;
map [ " errCt " ] = client - > info . errCt ;
2019-11-17 12:38:36 +02:00
map [ " nRequestsRcv " ] = client - > info . nRequestsRcv ;
2019-12-18 17:53:16 +02:00
map [ " isSubscribedToHeaders " ] = client - > isSubscribedToHeaders ;
2019-12-30 14:58:16 +02:00
map [ " nSubscriptions " ] = client - > nShSubs . load ( ) ;
2019-11-16 23:33:40 +02:00
// the below don't really make much sense for this class (they are always 0 or empty)
map . remove ( " nDisconnects " ) ;
map . remove ( " nSocketErrors " ) ;
map . remove ( " lastSocketError " ) ;
2019-11-17 12:38:36 +02:00
map . remove ( " nUnansweredRequests " ) ;
map . remove ( " nRequestsSent " ) ;
2019-11-16 22:27:21 +02:00
clientList . append ( QVariantMap ( { { name , map } } ) ) ;
}
ret [ " clients " ] = clientList ;
2019-11-09 21:14:07 +02:00
return ret ;
}
2019-11-07 23:55:13 +02:00
void Server : : on_started ( )
2019-05-09 02:12:58 +03:00
{
AbstractTcpServer : : on_started ( ) ;
2019-12-24 02:01:55 +02:00
// add more conns here, etc, if needed
2019-05-09 02:12:58 +03:00
}
2019-11-07 23:55:13 +02:00
void Server : : on_newConnection ( QTcpSocket * sock ) { newClient ( sock ) ; }
2019-05-09 02:12:58 +03:00
2019-04-27 22:43:09 +03:00
Client *
2019-11-07 23:55:13 +02:00
Server : : newClient ( QTcpSocket * sock )
2019-04-27 22:43:09 +03:00
{
2019-05-02 07:10:25 +03:00
const auto clientId = newId ( ) ;
2019-11-09 00:04:28 +02:00
auto ret = clientsById [ clientId ] = new Client ( rpcMethods ( ) , clientId , this , sock ) ;
2019-04-27 22:43:09 +03:00
// if deleted, we need to purge it from map
2019-05-02 07:10:25 +03:00
auto on_destroyed = [ clientId , this ] ( QObject * o ) {
2019-04-27 22:43:09 +03:00
// this whole call is here so that delete client->sock ends up auto-removing the map entry
// as a convenience.
2019-04-30 13:31:56 +03:00
Debug ( ) < < " Client nested 'on_destroyed' called " ;
2019-05-02 07:10:25 +03:00
auto client = clientsById . take ( clientId ) ;
2019-04-27 22:43:09 +03:00
if ( client ) {
if ( client ! = o ) {
Error ( ) < < " client != passed-in pointer to on_destroy in " < < __FILE__ < < " line " < < __LINE__ < < " . FIXME! " ;
}
2019-05-02 07:10:25 +03:00
Debug ( " client id %ld purged from map " , long ( clientId ) ) ;
2019-04-27 22:43:09 +03:00
}
} ;
connect ( ret , & QObject : : destroyed , this , on_destroyed ) ;
2019-05-02 07:10:25 +03:00
connect ( ret , & AbstractConnection : : lostConnection , this , [ this , clientId ] ( AbstractConnection * cl ) {
2019-04-28 14:59:01 +03:00
if ( auto client = dynamic_cast < Client * > ( cl ) ; client ) {
Debug ( ) < < client - > prettyName ( ) < < " lost connection " ;
2019-05-04 09:37:42 +03:00
emit clientDisconnected ( client - > id ) ;
2019-04-28 14:59:01 +03:00
killClient ( client ) ;
} else {
2019-05-02 07:10:25 +03:00
Error ( ) < < " Internal error: lostConnection callback received null client! (expected client id: " < < clientId < < " ) " ;
2019-04-28 14:59:01 +03:00
}
} ) ;
2019-11-09 17:06:40 +02:00
connect ( ret , & RPC : : ConnectionBase : : gotMessage , this , & Server : : onMessage ) ;
connect ( ret , & RPC : : ConnectionBase : : gotErrorMessage , this , & Server : : onErrorMessage ) ;
connect ( ret , & RPC : : ConnectionBase : : peerError , this , & Server : : onPeerError ) ;
2019-04-27 22:43:09 +03:00
return ret ;
}
2019-11-07 23:55:13 +02:00
void Server : : killClient ( Client * client )
2019-04-27 22:43:09 +03:00
{
if ( ! client )
return ;
2019-04-28 14:59:01 +03:00
Debug ( ) < < __FUNCTION__ < < " (id: " < < client - > id < < " ) " ;
2019-04-27 22:43:09 +03:00
clientsById . remove ( client - > id ) ; // ensure gone from map asap so future lookups fail
2019-04-30 19:21:47 +03:00
client - > do_disconnect ( ) ;
2019-04-27 22:43:09 +03:00
}
2019-11-12 22:14:04 +02:00
void Server : : killClient ( quint64 clientId )
2019-04-27 22:43:09 +03:00
{
2019-05-02 07:10:25 +03:00
killClient ( clientsById . take ( clientId ) ) ;
2019-04-27 22:43:09 +03:00
}
2019-11-12 22:14:04 +02:00
void Server : : onMessage ( quint64 clientId , const RPC : : Message & m )
2019-05-02 23:01:43 +03:00
{
2019-11-11 00:22:31 +02:00
Trace ( ) < < " onMessage: " < < clientId < < " json: " < < m . toJsonString ( ) ;
2019-05-02 23:01:43 +03:00
if ( Client * c = getClient ( clientId ) ; c ) {
2019-11-09 00:04:28 +02:00
auto member = StaticData : : dispatchTable . value ( m . method ) ;
2019-11-08 16:34:20 +02:00
if ( ! member )
Error ( ) < < " Unknown method: \" " < < m . method < < " \" . This shouldn't happen. FIXME! Json: " < < m . toJsonString ( ) ;
2019-11-16 23:33:40 +02:00
else {
// indicate a good request, accepted request
2019-11-17 12:38:36 +02:00
+ + c - > info . nRequestsRcv ;
2019-12-21 18:55:48 +02:00
try {
// call ptr to member -- note member is free to throw if it wants to send an error immediately
( this - > * member ) ( c , m ) ;
} catch ( const RPCError & e ) {
emit c - > sendError ( false , e . code , e . what ( ) , m . id ) ;
} catch ( const std : : exception & e ) {
emit c - > sendError ( false , RPC : : ErrorCodes : : Code_InternalError ,
QString ( " internal error: %1 " ) . arg ( e . what ( ) ) , m . id ) ;
} catch ( . . . ) {
emit c - > sendError ( false , RPC : : ErrorCodes : : Code_InternalError , " internal error: unknown " , m . id ) ;
}
2019-11-16 23:33:40 +02:00
}
2019-05-02 23:01:43 +03:00
} else {
Debug ( ) < < " Unknown client: " < < clientId ;
}
}
2019-11-12 22:14:04 +02:00
void Server : : onErrorMessage ( quint64 clientId , const RPC : : Message & m )
2019-05-02 23:01:43 +03:00
{
2019-11-11 00:22:31 +02:00
Trace ( ) < < " onErrorMessage: " < < clientId < < " json: " < < m . toJsonString ( ) ;
2019-11-08 02:49:09 +02:00
if ( Client * c = getClient ( clientId ) ; c ) {
2019-11-09 03:30:10 +02:00
// we never expect client to send us errors. Always return invalid request, disconnect client.
2019-11-09 03:26:21 +02:00
emit c - > sendError ( true , RPC : : Code_InvalidRequest , " Not a valid request object " ) ;
2019-11-08 02:49:09 +02:00
}
2019-05-02 23:01:43 +03:00
}
2019-11-12 22:14:04 +02:00
void Server : : onPeerError ( quint64 clientId , const QString & what )
2019-05-02 23:01:43 +03:00
{
Debug ( ) < < " onPeerError, client " < < clientId < < " error: " < < what ;
if ( Client * c = getClient ( clientId ) ; c ) {
2019-11-17 12:38:36 +02:00
if ( + + c - > info . errCt - c - > info . nRequestsRcv > = kMaxErrorCount ) {
2019-11-16 23:33:40 +02:00
Warning ( ) < < " Excessive errors ( " < < kMaxErrorCount < < " ) for: " < < c - > prettyName ( ) < < " , disconnecting " ;
2019-05-02 23:01:43 +03:00
killClient ( c ) ;
return ;
}
}
}
2019-11-08 16:34:20 +02:00
// --- RPC METHODS ---
2019-12-21 13:24:53 +02:00
namespace {
Util : : ThreadPool : : FailFunc defaultTPFailFunc ( Client * c , const RPC : : Message : : Id & id ) {
return [ c , id ] ( const QString & what ) {
emit c - > sendError ( false , RPC : : Code_InternalError , QString ( " internal error: %1 " ) . arg ( what ) , id ) ;
} ;
}
BitcoinDMgr : : FailF defaultBDFailFunc ( Client * c , const RPC : : Message : : Id & id ) {
return [ c , id ] ( const RPC : : Message : : Id & , const QString & what ) {
emit c - > sendError ( false , RPC : : Code_InternalError , QString ( " internal error: %1 " ) . arg ( what ) , id ) ;
} ;
}
}
2019-12-21 15:09:02 +02:00
Server : : RPCError : : ~ RPCError ( ) { }
2019-12-21 18:55:48 +02:00
void Server : : generic_do_async ( Client * c , const RPC : : Message : : Id & reqId , const std : : function < QVariant ( ) > & work )
2019-12-21 15:09:02 +02:00
{
if ( LIKELY ( work ) ) {
struct ResErr {
QVariant results ;
bool error = false ;
QString errMsg ;
int errCode = 0 ;
} ;
2019-12-21 18:55:48 +02:00
auto reserr = std : : make_shared < ResErr > ( ) ; ///< shared with lambda for both work and completion. this is how they communicate.
2019-12-21 15:09:02 +02:00
Util : : ThreadPool : : SubmitWork (
c , // <--- all work done in client context, so if client is deleted, completion not called
// runs in worker thread, must not access anything other than reserr and work
[ reserr , work ] {
try {
QVariant result = work ( ) ;
reserr - > results . swap ( result ) ; // constant-time copy
} catch ( const RPCError & e ) {
reserr - > error = true ;
reserr - > errMsg = e . what ( ) ;
reserr - > errCode = e . code ;
}
} ,
// completion: runs in client thread (only called if client not already deleted)
2019-12-21 18:55:48 +02:00
[ c , reqId , reserr ] {
2019-12-21 15:09:02 +02:00
if ( reserr - > error ) {
2019-12-21 18:55:48 +02:00
emit c - > sendError ( false , reserr - > errCode , reserr - > errMsg , reqId ) ;
2019-12-21 15:09:02 +02:00
return ;
}
// no error, send results to client
2019-12-21 18:55:48 +02:00
emit c - > sendResult ( reqId , reserr - > results ) ;
2019-12-21 15:09:02 +02:00
} ,
2019-12-21 18:55:48 +02:00
// default fail function just sends json rpc error "internal error: <message>"
defaultTPFailFunc ( c , reqId )
2019-12-21 15:09:02 +02:00
) ;
} else
Error ( ) < < " INTERNAL ERROR: work must be valid! FIXME! " ;
}
2019-12-21 18:55:48 +02:00
void Server : : generic_async_to_bitcoind ( Client * c , const RPC : : Message : : Id & reqId , const QString & method ,
const QVariantList & params ,
const BitcoinDSuccessFunc & successFunc ,
const BitcoinDErrorFunc & errorFunc )
{
2019-12-23 18:36:36 +02:00
if ( UNLIKELY ( QThread : : currentThread ( ) ! = c - > thread ( ) ) ) {
// Paranoia, in case I or a future programmer forgets this rule.
Warning ( ) < < __FUNCTION__ < < " is meant to be called from the Client thread only. The current thread is not the "
< < " Client thread. This may cause problems if the Client is deleted while submitting the request. FIXME! " ;
}
2019-12-21 18:55:48 +02:00
bitcoindmgr - > submitRequest ( c , newId ( ) , method , params ,
// success
[ c , reqId , successFunc ] ( const RPC : : Message & reply ) {
try {
const QVariant result = successFunc ? successFunc ( reply ) : reply . result ( ) ; // if no successFunc specified, use default which just copies the result to the client.
emit c - > sendResult ( reqId , result ) ;
} catch ( const RPCError & e ) {
emit c - > sendError ( false , e . code , e . what ( ) , reqId ) ;
} catch ( const std : : exception & e ) {
emit c - > sendError ( false , RPC : : ErrorCodes : : Code_InternalError , e . what ( ) , reqId ) ;
}
} ,
// error
[ c , reqId , errorFunc ] ( const RPC : : Message & errorReply ) {
try {
if ( errorFunc )
errorFunc ( errorReply ) ; // this should throw RPCError
throw RPCError ( errorReply . errorMessage ( ) , RPC : : Code_App_DaemonError ) ;
} catch ( const RPCError & e ) {
emit c - > sendError ( false , e . code , e . what ( ) , reqId ) ;
} catch ( const std : : exception & e ) {
emit c - > sendError ( false , RPC : : ErrorCodes : : Code_InternalError , QString ( " internal error: %1 " ) . arg ( e . what ( ) ) ,
reqId ) ;
}
} ,
// use default function on failure, sends json rpc error "internal error: <message>"
defaultBDFailFunc ( c , reqId )
) ;
}
2019-12-23 09:36:52 +02:00
void Server : : rpc_server_banner ( Client * c , const RPC : : Message & m )
{
// TODO: have this come from a configurable text file, etc
2019-12-23 09:57:41 +02:00
emit c - > sendResult ( m . id , QString ( " Connected to a %1 %2 server " ) . arg ( APPNAME ) . arg ( VERSION ) ) ;
2019-12-23 09:36:52 +02:00
}
void Server : : rpc_server_donation_address ( Client * c , const RPC : : Message & m )
{
2019-12-23 09:57:41 +02:00
// TODO: have this come from a configuration param, etc
2019-12-23 09:36:52 +02:00
emit c - > sendResult ( m . id , QString ( " bitcoincash:qplw0d304x9fshz420lkvys2jxup38m9symky6k028 " ) ) ;
}
2019-12-23 11:12:29 +02:00
void Server : : rpc_server_features ( Client * c , const RPC : : Message & m )
{
// TODO: this is an incomplete impl. ("hosts", etc need love, see comments below)
QVariantMap r ;
r [ " pruning " ] = QVariant ( ) ; // null
r [ " genesis_hash " ] = QString ( Util : : ToHexFast ( storage - > genesisHash ( ) ) ) ;
r [ " server_version " ] = QString ( " %1 %2 " ) . arg ( APPNAME ) . arg ( VERSION ) ;
r [ " protocol_min " ] = " 1.4 " ; // TODO: have this come from a global constant
r [ " protocol_max " ] = " 1.4.2 " ; // TODO: have this come from a global constant
r [ " hash_function " ] = " sha256 " ;
// TODO: this "hosts" key is a stub. Its info needs to come from global config announce settings since public
// host/port is what we should return, not local bind ip, local bind port.
r [ " hosts " ] = QVariantMap {
{ c - > localAddress ( ) . toString ( ) , /* TODO: have this NOT be the bind ip but rather come from config "announce" setting */
QVariantMap {
{ " tcp_port " , c - > localPort ( ) /* TODO: have this NOT be the bind ip but rather come from config "announce" setting */ }
}
// TODO: support SSL
} ,
} ;
emit c - > sendResult ( m . id , r ) ;
}
2019-12-24 02:01:55 +02:00
void Server : : rpc_server_peers_subscribe ( Client * c , const RPC : : Message & m )
{
// TODO: Implement. See: https://electrumx.readthedocs.io/en/latest/protocol-methods.html#server-peers-subscribe
emit c - > sendResult ( m . id , QVariantList ( ) ) ;
}
2019-12-23 09:36:52 +02:00
void Server : : rpc_server_ping ( Client * c , const RPC : : Message & m )
{
emit c - > sendResult ( m . id ) ;
}
2019-11-08 16:34:20 +02:00
void Server : : rpc_server_version ( Client * c , const RPC : : Message & m )
{
2019-12-21 18:55:48 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( l . size ( ) = = 2 ) ;
c - > info . userAgent = l [ 0 ] . toString ( ) . left ( kMaxServerVersion ) ;
c - > info . protocolVersion = l [ 1 ] . toString ( ) . left ( kMaxServerVersion ) ;
emit c - > sendResult ( m . id , QStringList ( { QString ( " %1/%2 " ) . arg ( APPNAME ) . arg ( VERSION ) , QString ( " 1.4 " ) } ) ) ;
2019-11-08 16:34:20 +02:00
}
2019-12-18 17:53:16 +02:00
2019-12-22 16:54:26 +02:00
/// returns the 'branch' and 'root' keys ready to be put in the results dictionary
auto Server : : getHeadersBranchAndRoot ( unsigned height , unsigned cp_height ) - > HeadersBranchAndRootPair
{
HeadersBranchAndRootPair ret ;
if ( height < = cp_height ) {
auto pair = storage - > headerBranchAndRoot ( height , cp_height ) ;
auto & [ branch , root ] = pair ;
std : : reverse ( root . begin ( ) , root . end ( ) ) ;
2019-12-23 09:12:17 +02:00
ret . second = QString ( Util : : ToHexFast ( root ) ) ; // we cast to QString to prevent JSON null for empty string ""
2019-12-22 16:54:26 +02:00
QVariantList & branchList = ret . first ;
branchList . reserve ( int ( branch . size ( ) ) ) ;
for ( auto & item : branch ) {
std : : reverse ( item . begin ( ) , item . end ( ) ) ;
branchList . push_back ( Util : : ToHexFast ( item ) ) ;
if ( branchList . back ( ) . isNull ( ) )
// this should never happen, but it pays to be paranoid
throw InternalError ( " bad data retrieved from merkle cache " ) ;
}
}
return ret ;
}
2019-12-18 13:04:40 +02:00
void Server : : rpc_blockchain_block_header ( Client * c , const RPC : : Message & m )
{
2019-12-21 15:09:02 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( ! l . isEmpty ( ) ) ;
bool ok ;
const unsigned height = l . front ( ) . toUInt ( & ok ) ;
2019-12-21 18:55:48 +02:00
if ( ! ok | | height > = Storage : : MAX_HEADERS )
throw RPCError ( " Invalid height " ) ;
ok = true ;
2019-12-21 15:09:02 +02:00
const unsigned cp_height = l . size ( ) > 1 ? l . back ( ) . toUInt ( & ok ) : 0 ;
2019-12-21 19:51:33 +02:00
if ( ! ok | | cp_height > = Storage : : MAX_HEADERS )
throw RPCError ( " Invalid cp_height " ) ;
if ( cp_height ) {
const auto tip = storage - > latestTip ( ) . first ;
2019-12-22 02:11:16 +02:00
if ( tip < 0 ) throw InternalError ( " chain height is negative " ) ;
2019-12-21 19:51:33 +02:00
if ( ! ( height < = cp_height & & cp_height < = unsigned ( tip ) ) )
throw RPCError ( QString ( " header height %1 must be <= cp_height %2 which must be <= chain height %3 " )
. arg ( height ) . arg ( cp_height ) . arg ( tip ) ) ;
}
2019-12-21 18:55:48 +02:00
generic_do_async ( c , m . id , [ height , cp_height , this ] {
2019-12-18 17:53:16 +02:00
QString err ;
2019-12-23 09:12:17 +02:00
const auto optHdr = storage - > headerForHeight ( height , & err ) ; // may return nothing (but will set err) if height is now beyond chain height due to reorg
2019-12-22 01:20:24 +02:00
if ( QByteArray hdr ; err . isEmpty ( ) & & optHdr . has_value ( ) & & ! ( hdr = optHdr . value ( ) ) . isEmpty ( ) ) {
2019-12-23 09:12:17 +02:00
const auto hexHdr = Util : : ToHexFast ( hdr ) ; // hexHdr definitely not empty, no need to cast to QString (avoids a copy!)
2019-12-22 01:20:24 +02:00
QVariant ret ;
if ( ! cp_height )
ret = hexHdr ;
else {
2019-12-23 09:12:17 +02:00
const auto [ branch , root ] = getHeadersBranchAndRoot ( height , cp_height ) ; // may throw if chain mutated and height or cp_height are no longer legal
2019-12-22 01:20:24 +02:00
ret = QVariantMap {
{ " header " , hexHdr } ,
2019-12-22 16:54:26 +02:00
{ " branch " , branch } ,
{ " root " , root } ,
2019-12-22 01:20:24 +02:00
} ;
}
return ret ;
} else
2019-12-21 15:09:02 +02:00
throw RPCError ( err . isEmpty ( ) ? " Unknown error " : err ) ;
} ) ;
2019-12-18 13:04:40 +02:00
}
2019-12-21 15:09:02 +02:00
2019-12-18 13:04:40 +02:00
void Server : : rpc_blockchain_block_headers ( Client * c , const RPC : : Message & m )
{
2019-12-21 15:09:02 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( l . size ( ) > = 2 ) ;
bool ok ;
const unsigned height = l . front ( ) . toUInt ( & ok ) ;
2019-12-21 18:55:48 +02:00
if ( ! ok | | height > = Storage : : MAX_HEADERS )
throw RPCError ( " Invalid height " ) ;
2019-12-22 16:54:26 +02:00
unsigned count = l [ 1 ] . toUInt ( & ok ) ;
2019-12-21 18:55:48 +02:00
if ( ! ok | | count > = Storage : : MAX_HEADERS )
throw RPCError ( " Invalid count " ) ;
2019-12-22 16:54:26 +02:00
const auto tip = storage - > latestTip ( ) . first ;
if ( tip < 0 ) throw InternalError ( " chain height is negative " ) ;
static constexpr unsigned MAX_COUNT = 2016 ; ///< TODO: make this cofigurable. this is the current electrumx limit, for now.
count = std : : min ( std : : min ( unsigned ( tip + 1 ) - height , count ) , MAX_COUNT ) ;
2019-12-21 15:09:02 +02:00
ok = true ;
const unsigned cp_height = l . size ( ) > 2 ? l . back ( ) . toUInt ( & ok ) : 0 ;
2019-12-22 16:54:26 +02:00
if ( ! ok | | cp_height > = Storage : : MAX_HEADERS )
throw RPCError ( " Invalid cp_height " ) ;
if ( count & & cp_height ) {
if ( ! ( height + ( count - 1 ) < = cp_height & & cp_height < = unsigned ( tip ) ) )
throw RPCError ( QString ( " header height + (count - 1) %1 must be <= cp_height %2 which must be <= chain height %3 " )
. arg ( height + ( count - 1 ) ) . arg ( cp_height ) . arg ( tip ) ) ;
}
generic_do_async ( c , m . id , [ height , count , cp_height , this ] {
2019-12-18 13:04:40 +02:00
// EX doesn't seem to return error here if invalid height/no results, so we will do same.
2019-12-22 16:54:26 +02:00
const auto hdrs = storage - > headersFromHeight ( height , std : : min ( count , MAX_COUNT ) ) ;
2019-12-18 13:04:40 +02:00
const size_t nHdrs = hdrs . size ( ) , hdrSz = size_t ( BTC : : GetBlockHeaderSize ( ) ) , hdrHexSz = hdrSz * 2 ;
QByteArray hexHeaders ( int ( nHdrs * hdrHexSz ) , Qt : : Uninitialized ) ;
for ( size_t i = 0 , offset = 0 ; i < nHdrs ; + + i , offset + = hdrHexSz ) {
const auto & hdr = hdrs [ i ] ;
if ( UNLIKELY ( hdr . size ( ) ! = int ( hdrSz ) ) ) { // ensure header looks the right size
// this should never happen.
Error ( ) < < " Header size from db height " < < i + height < < " is not " < < hdrSz < < " bytes! Database corruption likely! FIXME! " ;
2019-12-23 09:12:17 +02:00
throw RPCError ( " Server header store invalid " , RPC : : Code_InternalError ) ;
2019-12-18 13:04:40 +02:00
}
// fast, in-place conversion to hex
Util : : ToHexFastInPlace ( hdr , hexHeaders . data ( ) + offset , hdrHexSz ) ;
}
QVariantMap resp {
2019-12-23 09:12:17 +02:00
{ " hex " , QString ( hexHeaders ) } , // we cast to QString to prevent null for empty string ""
2019-12-18 13:04:40 +02:00
{ " count " , unsigned ( hdrs . size ( ) ) } ,
2019-12-21 18:55:48 +02:00
{ " max " , MAX_COUNT }
2019-12-18 13:04:40 +02:00
} ;
2019-12-22 16:54:26 +02:00
if ( count & & cp_height ) {
2019-12-23 09:12:17 +02:00
// Note: it's possible for a reorg to happen and the chain height to be shortened in parellel in such
// a way that lastHeight > chainHeight or cp_height > chainHeight, thus making this merkle branch query
// below illegal. In that case the getHeadersBranchAndRoot function will bubble up an exception about a
// short header count, which is what we want.
2019-12-22 16:54:26 +02:00
const auto lastHeight = height + count - 1 ;
const auto [ branch , root ] = getHeadersBranchAndRoot ( lastHeight , cp_height ) ;
resp [ " branch " ] = branch ;
resp [ " root " ] = root ;
}
2019-12-21 15:09:02 +02:00
return resp ;
} ) ;
2019-12-18 13:04:40 +02:00
}
2019-12-18 17:53:16 +02:00
void Server : : rpc_blockchain_estimatefee ( Client * c , const RPC : : Message & m )
{
2019-12-21 18:55:48 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( ! l . isEmpty ( ) ) ;
constexpr double dummyReply = 0.00001000 ;
emit c - > sendResult ( m . id , dummyReply ) ;
2019-12-18 17:53:16 +02:00
}
void Server : : rpc_blockchain_headers_subscribe ( Client * c , const RPC : : Message & m ) // fully implemented
{
2019-12-21 18:55:48 +02:00
// helper used both for this response and for notifications
static const auto mkResp = [ ] ( unsigned height , const QByteArray & header ) - > QVariantMap {
return QVariantMap {
{ " height " , height } ,
{ " hex " , Util : : ToHexFast ( header ) }
2019-12-18 17:53:16 +02:00
} ;
2019-12-21 18:55:48 +02:00
} ;
Storage : : Header hdr ;
const auto [ height , hhash ] = storage - > latestTip ( & hdr ) ;
// we assume everything is peachy and don't check header size, etc as we can't really get here until we have synched at least *some* headers.
if ( ! c - > isSubscribedToHeaders ) {
c - > isSubscribedToHeaders = true ;
// connect to signal. Will be emitted directly to object until it dies.
connect ( this , & Server : : newHeader , c , [ c , meth = m . method ] ( unsigned height , const QByteArray & header ) {
2019-12-24 02:44:08 +02:00
// the notification is a list of size 1, with a dict in it. :/
c - > sendNotification ( meth , QVariantList ( { mkResp ( height , header ) } ) ) ;
2019-12-21 18:55:48 +02:00
} ) ;
2019-12-30 02:07:22 +02:00
Debug ( ) < < c - > prettyName ( false , false ) < < " is now subscribed to headers " ;
2019-12-18 17:53:16 +02:00
} else {
2019-12-30 02:07:22 +02:00
Debug ( ) < < c - > prettyName ( false , false ) < < " was already subscribed to headers, ignoring duplicate subscribe request " ;
2019-12-18 17:53:16 +02:00
}
2019-12-21 18:55:48 +02:00
emit c - > sendResult ( m . id , mkResp ( unsigned ( std : : max ( 0 , height ) ) , hdr ) ) ;
2019-12-18 17:53:16 +02:00
}
void Server : : rpc_blockchain_relayfee ( Client * c , const RPC : : Message & m )
{
// TODO: Implement this
constexpr double dummyReply = 0.00001000 ;
2019-12-21 18:55:48 +02:00
emit c - > sendResult ( m . id , dummyReply ) ;
2019-12-18 17:53:16 +02:00
}
void Server : : rpc_blockchain_scripthash_get_balance ( Client * c , const RPC : : Message & m )
{
2019-12-21 15:09:02 +02:00
QVariantList l ( m . paramsList ( ) ) ;
assert ( ! l . isEmpty ( ) ) ;
const QByteArray sh = validateHashHex ( l . front ( ) . toString ( ) ) ;
2019-12-21 18:55:48 +02:00
if ( sh . length ( ) ! = HashLen )
throw RPCError ( " Invalid scripthash " ) ;
generic_do_async ( c , m . id , [ sh , this ] {
2019-12-28 16:13:24 +02:00
const auto [ amt , uamt ] = storage - > getBalance ( sh ) ;
2019-12-18 17:53:16 +02:00
/* Note: ElectrumX protocol docs are incorrect. They claim a string in coin units is returned here.
* It is not . Instead a number in satoshis is returned !
* Incorrect docs : https : //electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-balance */
QVariantMap resp {
{ " confirmed " , qlonglong ( amt / amt . satoshi ( ) ) } ,
2019-12-28 16:13:24 +02:00
{ " unconfirmed " , qlonglong ( uamt / uamt . satoshi ( ) ) } ,
2019-12-18 17:53:16 +02:00
} ;
2019-12-21 15:09:02 +02:00
return resp ;
} ) ;
2019-12-18 17:53:16 +02:00
}
2019-12-28 15:10:07 +02:00
/// called from get_mempool and get_history to retrieve the mempool for a hashx synchronously. Returns the
/// QVariantMap suitable for placing into the resulting response.
QVariantList Server : : getHistoryCommon ( const QByteArray & sh , bool mempoolOnly )
{
QVariantList resp ;
const auto items = storage - > getHistory ( sh , ! mempoolOnly , true ) ; // these are already sorted
for ( const auto & item : items ) {
QVariantMap m {
{ " tx_hash " , Util : : ToHexFast ( item . hash ) } ,
{ " height " , int ( item . height ) } ,
} ;
if ( item . fee . has_value ( ) )
m [ " fee " ] = qlonglong ( item . fee . value ( ) / bitcoin : : Amount : : satoshi ( ) ) ;
resp . push_back ( m ) ;
}
return resp ;
}
2019-12-18 17:53:16 +02:00
void Server : : rpc_blockchain_scripthash_get_history ( Client * c , const RPC : : Message & m )
{
2019-12-21 15:09:02 +02:00
QVariantList l ( m . paramsList ( ) ) ;
assert ( ! l . isEmpty ( ) ) ;
const QByteArray sh = validateHashHex ( l . front ( ) . toString ( ) ) ;
2019-12-21 18:55:48 +02:00
if ( sh . length ( ) ! = HashLen )
throw RPCError ( " Invalid scripthash " ) ;
generic_do_async ( c , m . id , [ sh , this ] {
2019-12-28 15:10:07 +02:00
return getHistoryCommon ( sh , false ) ;
2019-12-21 15:09:02 +02:00
} ) ;
2019-12-18 17:53:16 +02:00
}
2019-12-24 02:01:55 +02:00
void Server : : rpc_blockchain_scripthash_get_mempool ( Client * c , const RPC : : Message & m )
2019-12-19 14:29:48 +02:00
{
2019-12-21 18:55:48 +02:00
QVariantList l ( m . paramsList ( ) ) ;
assert ( ! l . isEmpty ( ) ) ;
const QByteArray sh = validateHashHex ( l . front ( ) . toString ( ) ) ;
if ( sh . length ( ) ! = HashLen )
throw RPCError ( " Invalid scripthash " ) ;
generic_do_async ( c , m . id , [ sh , this ] {
2019-12-28 15:10:07 +02:00
return getHistoryCommon ( sh , true ) ;
2019-12-21 18:55:48 +02:00
} ) ;
2019-12-19 14:29:48 +02:00
}
void Server : : rpc_blockchain_scripthash_listunspent ( Client * c , const RPC : : Message & m )
{
2019-12-21 15:09:02 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( ! l . isEmpty ( ) ) ;
const QByteArray sh = validateHashHex ( l . front ( ) . toString ( ) ) ;
2019-12-21 18:55:48 +02:00
if ( sh . length ( ) ! = HashLen )
throw RPCError ( " Invalid scripthash " ) ;
generic_do_async ( c , m . id , [ sh , this ] {
2019-12-19 14:29:48 +02:00
QVariantList resp ;
const auto items = storage - > listUnspent ( sh ) ; // these are already sorted
for ( const auto & item : items ) {
resp . push_back ( QVariantMap {
{ " tx_hash " , Util : : ToHexFast ( item . hash ) } ,
{ " tx_pos " , item . tx_pos } ,
{ " height " , item . height } , // confirmed height. TODO: should be 0 for mempool regardless of unconf. parent status. Note this differs from get_mempool or get_history
{ " value " , qlonglong ( item . value / item . value . satoshi ( ) ) } , // amount (int64) in satoshis
} ) ;
}
2019-12-21 15:09:02 +02:00
return resp ;
} ) ;
2019-12-19 14:29:48 +02:00
}
2019-11-08 16:34:20 +02:00
void Server : : rpc_blockchain_scripthash_subscribe ( Client * c , const RPC : : Message & m )
{
2019-12-21 18:55:48 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( l . size ( ) = = 1 ) ;
2019-12-24 02:01:55 +02:00
// this isn't really implemented. this is a stub that just returns the immediate status, but doesn't actually
// subscribe for notifications.
QByteArray sh = validateHashHex ( l . front ( ) . toString ( ) ) ;
if ( sh . length ( ) ! = HashLen )
throw RPCError ( " Invalid scripthash " ) ;
2019-12-29 22:57:04 +02:00
/// Note: potential race condition here whereby notification can arrive BEFORE the status result! FIXME!
2019-12-30 22:29:29 +02:00
const auto [ wasNew , optStatus ] = storage - > subs ( ) - > subscribe ( c , sh , [ c , method = m . method ] ( const HashX & sh , const StatusHash & status ) {
2019-12-30 10:42:10 +02:00
QVariant statusHexMaybeNull ; // if empty we simply notify as 'null' (this is unlikely in practice but may happen on reorg)
if ( ! status . isEmpty ( ) )
statusHexMaybeNull = Util : : ToHexFast ( status ) ;
const QByteArray shHex = Util : : ToHexFast ( sh ) ;
emit c - > sendNotification ( method , QVariantList { shHex , statusHexMaybeNull } ) ;
2019-12-29 22:57:04 +02:00
} ) ;
2019-12-30 14:58:16 +02:00
if ( wasNew ) + + c - > nShSubs ;
2019-12-30 22:29:29 +02:00
if ( ! optStatus . has_value ( ) ) {
// no known/cached status -- do the work ourselves asynch in the thread pool.
generic_do_async ( c , m . id , [ sh , this ] {
// TODO: send the computed value back to the SubsMgr somehow here? (This is tricky because race conditions)
QVariant ret ;
auto status = storage - > subs ( ) - > getFullStatus ( sh ) ;
if ( ! status . isEmpty ( ) ) // if empty we return `null`, otherwise we return hex encoded bytes as the immediate status.
ret = Util : : ToHexFast ( status ) ;
return ret ;
} ) ;
} else {
// SubsMgr reported a cached status -- immediately return that as the result!
QVariant result ;
if ( ! optStatus . value ( ) . isEmpty ( ) )
// not empty, so we return the hex-encoded string
result = QString ( Util : : ToHexFast ( optStatus . value ( ) ) ) ;
Debug ( ) < < " Sending cached status to client for scripthash: " < < Util : : ToHexFast ( sh ) < < " status: " < < result . toString ( ) ; // remove/comment-out this after testing complete
emit c - > sendResult ( m . id , result ) ; ///< may be 'null' if status was empty (indicates no history for scripthash)
}
2019-12-19 14:29:48 +02:00
}
void Server : : rpc_blockchain_scripthash_unsubscribe ( Client * c , const RPC : : Message & m )
{
2019-12-21 18:55:48 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( l . size ( ) = = 1 ) ;
QByteArray sh = validateHashHex ( l . front ( ) . toString ( ) ) ;
if ( sh . length ( ) ! = HashLen )
throw RPCError ( " Invalid scripthash " ) ;
2019-12-30 10:33:35 +02:00
const bool result = storage - > subs ( ) - > unsubscribe ( c , sh ) ;
2019-12-30 14:58:16 +02:00
if ( result ) - - c - > nShSubs ;
2019-12-29 22:57:04 +02:00
emit c - > sendResult ( m . id , QVariant ( result ) ) ;
2019-12-19 14:29:48 +02:00
}
void Server : : rpc_blockchain_transaction_broadcast ( Client * c , const RPC : : Message & m )
{
2019-12-21 18:55:48 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( l . size ( ) = = 1 ) ;
QByteArray rawtxhex = l . front ( ) . toString ( ) . left ( kMaxTxHex ) . toUtf8 ( ) ; // limit raw hex to sane length.
// no need to validate hex here -- bitcoind does validation for us!
generic_async_to_bitcoind ( c , m . id , " sendrawtransaction " , QVariantList { rawtxhex } ,
2019-12-31 16:22:52 +02:00
// print to log, echo bitcoind's reply to client
[ size = rawtxhex . length ( ) / 2 , cid = c - > id ] ( const RPC : : Message & reply ) {
const QVariant ret = reply . result ( ) ;
Log ( ) < < " Broadcast tx for client " < < cid < < " , size: " < < size < < " bytes, response: " < < ret . toString ( ) ;
return ret ;
} ,
2019-12-21 18:55:48 +02:00
// error func, throw an RPCError that's formatted in a particular way
[ ] ( const RPC : : Message & errResponse ) {
throw RPCError ( QString ( " the transaction was rejected by network rules. \n \n "
// Note: ElectrumX here would also spit back the [txhex] after the final newline.
// We do not do that, since it's a waste of bandwidth and also Electron Cash
// ignores that information anyway.
" %1 \n " ) . arg ( errResponse . errorMessage ( ) ) ,
RPC : : Code_App_BadRequest /**< ex does this here.. inconsistent with transaction.get,
* so for now we emulate that until we verify that EC
* will be ok with us changing it to Code_App_DaemonError */
) ;
}
) ;
// <-- do nothing right now, return without replying. Will respond when daemon calls us back in callbacks above.
2019-12-19 14:29:48 +02:00
}
void Server : : rpc_blockchain_transaction_get ( Client * c , const RPC : : Message & m )
{
2019-12-21 18:55:48 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( l . size ( ) < = 2 ) ;
QByteArray txHash = validateHashHex ( l . front ( ) . toString ( ) ) ;
if ( txHash . length ( ) ! = HashLen )
throw RPCError ( " Invalid tx hash " ) ;
bool verbose = false ;
if ( l . size ( ) = = 2 ) {
const auto [ verbArg , verbArgOk ] = parseBoolSemiLooselyButNotTooLoosely ( l . back ( ) ) ;
if ( ! verbArgOk )
throw RPCError ( " Invalid verbose argument; expected boolean " ) ;
verbose = verbArg ;
}
generic_async_to_bitcoind ( c , m . id , " getrawtransaction " , QVariantList { Util : : ToHexFast ( txHash ) , verbose } ,
// use the default success func, which just echoes the bitcoind reply to the client
BitcoinDSuccessFunc ( ) ,
// error func, throw an RPCError
[ ] ( const RPC : : Message & errResponse ) {
// EX does this weird thing.. we do it too for now until we can verify not doing it won't break old EC
2019-12-23 09:55:27 +02:00
// clients... TODO: see if this can be removed in favor of a more canonical error message.
2019-12-21 18:55:48 +02:00
throw RPCError ( formatBitcoinDErrorResponseToLookLikeDumbElectrumXPythonRepr ( errResponse ) ,
RPC : : Code_App_DaemonError ) ;
2019-12-19 14:29:48 +02:00
}
2019-12-21 18:55:48 +02:00
) ;
// <-- do nothing right now, return without replying. Will respond when daemon calls us back in callbacks above.
2019-12-19 14:29:48 +02:00
}
2019-12-20 19:12:05 +02:00
namespace {
2019-12-23 09:55:27 +02:00
/// Note: pos must be within the txHashes array, otherwise a BadArgs exception will be thrown.
2019-12-20 20:42:45 +02:00
/// Input txHashes should be in bitcoind memory order.
/// Output is a QVariantList already reversed and hex encoded, suitable for putting into the results map as 'merkle'.
/// Used by the below two _id_from_pos and _get_merkle rpc methods.
QVariantList getMerkleForTxHashes ( const std : : vector < QByteArray > & txHashes , unsigned pos ) {
2019-12-20 19:12:05 +02:00
QVariantList branchList ;
// next, compute the branch and root for the tx hashes which are now in bitcoind memory order
auto pair = Merkle : : branchAndRoot ( txHashes , pos ) ;
auto & [ branch , root ] = pair ;
// now, build our results for json as a QVariantList, reversing the memory back to hex memory order, and hex encoding it.
branchList . reserve ( int ( branch . size ( ) ) ) ;
for ( auto & h : branch ) {
// reverse each hash in place and then hex encode it, and pust it to branchList
std : : reverse ( h . begin ( ) , h . end ( ) ) ;
h = Util : : ToHexFast ( h ) ;
branchList . push_back ( h ) ;
}
return branchList ;
}
}
2019-12-19 14:29:48 +02:00
void Server : : rpc_blockchain_transaction_get_merkle ( Client * c , const RPC : : Message & m )
{
2019-12-21 15:09:02 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( l . size ( ) = = 2 ) ;
QByteArray txHash = validateHashHex ( l . front ( ) . toString ( ) ) ;
2019-12-21 18:55:48 +02:00
if ( txHash . length ( ) ! = HashLen )
throw RPCError ( " Invalid tx hash " ) ;
2019-12-21 15:09:02 +02:00
bool ok = false ;
unsigned height = l . back ( ) . toUInt ( & ok ) ;
2019-12-21 18:55:48 +02:00
if ( ! ok | | height > = Storage : : MAX_HEADERS )
throw RPCError ( " Invalid height argument; expected non-negative numeric value " ) ;
generic_do_async ( c , m . id , [ txHash , height , this ] ( ) mutable {
2019-12-20 20:42:45 +02:00
auto txHashes = storage - > txHashesForBlockInBitcoindMemoryOrder ( height ) ;
std : : reverse ( txHash . begin ( ) , txHash . end ( ) ) ; // we need to compare to bitcoind memory order so reverse specified hash
2019-12-20 19:12:05 +02:00
constexpr unsigned NO_POS = ~ 0U ;
unsigned pos = NO_POS ;
for ( unsigned i = 0 ; i < txHashes . size ( ) ; + + i ) {
if ( txHashes [ i ] = = txHash ) {
pos = i ;
break ;
}
}
2019-12-21 15:09:02 +02:00
if ( pos = = NO_POS )
throw RPCError ( QString ( " No transaction matching the requested hash found at height %1 " ) . arg ( height ) ) ;
2019-12-20 19:12:05 +02:00
const auto branchList = getMerkleForTxHashes ( txHashes , pos ) ;
QVariantMap resp = {
{ " block_height " , height } ,
{ " pos " , pos } ,
{ " merkle " , branchList }
} ;
2019-12-21 15:09:02 +02:00
return resp ;
} ) ;
2019-12-19 14:29:48 +02:00
}
2019-12-20 19:12:05 +02:00
2019-12-19 14:29:48 +02:00
void Server : : rpc_blockchain_transaction_id_from_pos ( Client * c , const RPC : : Message & m )
{
2019-12-21 15:09:02 +02:00
QVariantList l = m . paramsList ( ) ;
assert ( ! l . isEmpty ( ) ) ;
bool ok = false ;
unsigned height = l . front ( ) . toUInt ( & ok ) ; // arg0
2019-12-21 18:55:48 +02:00
if ( ! ok | | height > = Storage : : MAX_HEADERS )
throw RPCError ( " Invalid height argument; expected non-negative numeric value " ) ;
2019-12-21 15:09:02 +02:00
unsigned pos = l . at ( 1 ) . toUInt ( & ok ) ; // arg1
2019-12-21 18:55:48 +02:00
constexpr unsigned MAX_POS = Storage : : MAX_HEADERS ;
if ( ! ok | | pos > = MAX_POS )
throw RPCError ( " Invalid tx_pos argument; expected non-negative numeric value " ) ;
2019-12-21 15:09:02 +02:00
bool merkle = false ;
if ( l . size ( ) = = 3 ) { //optional arg2
const auto [ arg , argOk ] = parseBoolSemiLooselyButNotTooLoosely ( l . back ( ) ) ;
2019-12-21 18:55:48 +02:00
if ( ! argOk )
throw RPCError ( " Invalid merkle argument; expected boolean " ) ;
2019-12-21 15:09:02 +02:00
merkle = arg ;
}
2019-12-21 18:55:48 +02:00
generic_do_async ( c , m . id , [ height , pos , merkle , this ] {
static const QString missingErr ( " No transaction at position %1 for height %2 " ) ;
if ( merkle ) {
// merkle=true is a dict, see: https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-transaction-id-from-pos
2019-12-20 19:12:05 +02:00
// get all hashes for the block (we need them for merkle)
2019-12-20 20:42:45 +02:00
auto txHashes = storage - > txHashesForBlockInBitcoindMemoryOrder ( height ) ;
2019-12-20 19:12:05 +02:00
if ( pos > = txHashes . size ( ) ) {
// out of range, or block not found
2019-12-21 15:09:02 +02:00
throw RPCError ( missingErr . arg ( pos ) . arg ( height ) ) ;
2019-12-20 19:12:05 +02:00
}
// save the requested tx_hash now, which we will return as tx_hash of the response dictionary
2019-12-20 20:42:45 +02:00
// (we need to reverse it for outputting to hex since we received it in bitcoind internal memory order).
const QByteArray txHashHex = Util : : ToHexFast ( Util : : reversedCopy ( txHashes [ pos ] ) ) ;
2019-12-20 19:12:05 +02:00
const auto branchList = getMerkleForTxHashes ( txHashes , pos ) ;
QVariantMap res = {
{ " tx_hash " , txHashHex } ,
{ " merkle " , branchList } ,
} ;
2019-12-21 18:55:48 +02:00
return QVariant ( res ) ;
} else {
// no merkle, just return the tx_hash immediately without going async
const auto opt = storage - > hashForHeightAndPos ( height , pos ) ;
if ( ! opt . has_value ( ) | | opt . value ( ) . length ( ) ! = HashLen )
throw RPCError ( missingErr . arg ( pos ) . arg ( height ) ) ;
const auto txHashHex = Util : : ToHexFast ( opt . value ( ) ) ;
return QVariant ( txHashHex ) ;
2019-12-19 14:29:48 +02:00
}
2019-12-21 18:55:48 +02:00
} ) ;
2019-12-19 14:29:48 +02:00
}
void Server : : rpc_mempool_get_fee_histogram ( Client * c , const RPC : : Message & m )
{
2019-12-21 18:55:48 +02:00
// this is a stub
emit c - > sendResult ( m . id , QVariantList ( ) ) ;
2019-11-08 16:34:20 +02:00
}
2019-11-09 00:04:28 +02:00
// --- Server::StaticData Definitions ---
# define HEY_COMPILER_PUT_STATIC_HERE(x) decltype(x) x
2019-12-18 11:58:25 +02:00
# define PR RPC::Method::PosParamRange
2019-11-09 00:04:28 +02:00
HEY_COMPILER_PUT_STATIC_HERE ( Server : : StaticData : : dispatchTable ) ;
HEY_COMPILER_PUT_STATIC_HERE ( Server : : StaticData : : methodMap ) ;
HEY_COMPILER_PUT_STATIC_HERE ( Server : : StaticData : : registry ) {
/* ==> Note: Add stuff to this table when adding new RPC methods.
2019-12-18 17:53:16 +02:00
{ { " rpc.name " , allow_requests , allow_notifications , PosParamRange , ( QSet < QString > note : { } means undefined optional ) } , & method_to_call } */
2019-12-23 09:36:52 +02:00
{ { " server.banner " , true , false , PR { 0 , 0 } , RPC : : KeySet { } } , & Server : : rpc_server_banner } ,
{ { " server.donation_address " , true , false , PR { 0 , 0 } , } , & Server : : rpc_server_donation_address } ,
2019-12-23 11:12:29 +02:00
{ { " server.features " , true , false , PR { 0 , 0 } , } , & Server : : rpc_server_features } ,
2019-12-24 02:01:55 +02:00
{ { " server.peers.subscribe " , true , false , PR { 0 , 0 } , } , & Server : : rpc_server_peers_subscribe } ,
2019-12-23 09:36:52 +02:00
{ { " server.ping " , true , false , PR { 0 , 0 } , } , & Server : : rpc_server_ping } ,
2019-12-18 17:53:16 +02:00
{ { " server.version " , true , false , PR { 2 , 2 } , } , & Server : : rpc_server_version } ,
2019-12-19 14:29:48 +02:00
2019-12-18 17:53:16 +02:00
{ { " blockchain.block.header " , true , false , PR { 1 , 2 } , } , & Server : : rpc_blockchain_block_header } ,
{ { " blockchain.block.headers " , true , false , PR { 2 , 3 } , } , & Server : : rpc_blockchain_block_headers } ,
{ { " blockchain.estimatefee " , true , false , PR { 1 , 1 } , } , & Server : : rpc_blockchain_estimatefee } ,
{ { " blockchain.headers.subscribe " , true , false , PR { 0 , 0 } , } , & Server : : rpc_blockchain_headers_subscribe } ,
{ { " blockchain.relayfee " , true , false , PR { 0 , 0 } , } , & Server : : rpc_blockchain_relayfee } ,
2019-12-19 14:29:48 +02:00
2019-12-18 17:53:16 +02:00
{ { " blockchain.scripthash.get_balance " , true , false , PR { 1 , 1 } , } , & Server : : rpc_blockchain_scripthash_get_balance } ,
{ { " blockchain.scripthash.get_history " , true , false , PR { 1 , 1 } , } , & Server : : rpc_blockchain_scripthash_get_history } ,
2019-12-19 14:29:48 +02:00
{ { " blockchain.scripthash.get_mempool " , true , false , PR { 1 , 1 } , } , & Server : : rpc_blockchain_scripthash_get_mempool } ,
{ { " blockchain.scripthash.listunspent " , true , false , PR { 1 , 1 } , } , & Server : : rpc_blockchain_scripthash_listunspent } ,
2019-12-18 17:53:16 +02:00
{ { " blockchain.scripthash.subscribe " , true , false , PR { 1 , 1 } , } , & Server : : rpc_blockchain_scripthash_subscribe } ,
2019-12-19 14:29:48 +02:00
{ { " blockchain.scripthash.unsubscribe " , true , false , PR { 1 , 1 } , } , & Server : : rpc_blockchain_scripthash_unsubscribe } ,
{ { " blockchain.transaction.broadcast " , true , false , PR { 1 , 1 } , } , & Server : : rpc_blockchain_transaction_broadcast } ,
{ { " blockchain.transaction.get " , true , false , PR { 1 , 2 } , } , & Server : : rpc_blockchain_transaction_get } ,
{ { " blockchain.transaction.get_merkle " , true , false , PR { 2 , 2 } , } , & Server : : rpc_blockchain_transaction_get_merkle } ,
{ { " blockchain.transaction.id_from_pos " , true , false , PR { 2 , 3 } , } , & Server : : rpc_blockchain_transaction_id_from_pos } ,
{ { " mempool.get_fee_histogram " , true , false , PR { 0 , 0 } , } , & Server : : rpc_mempool_get_fee_histogram } ,
2019-11-08 17:11:58 +02:00
} ;
2019-12-18 11:58:25 +02:00
# undef PR
2019-11-09 00:04:28 +02:00
# undef HEY_COMPILER_PUT_STATIC_HERE
/*static*/
void Server : : StaticData : : init ( )
{
if ( ! dispatchTable . empty ( ) )
return ;
for ( const auto & r : registry ) {
if ( ! r . member ) {
Error ( ) < < " Runtime check failed: RPC Method " < < r . method < < " has a nullptr for its .member! See Server class! FIXME! " ;
std : : _Exit ( EXIT_FAILURE ) ;
}
methodMap [ r . method ] = r ;
dispatchTable [ r . method ] = r . member ;
}
}
// --- /Server::StaticData Definitions ---
2019-11-08 16:34:20 +02:00
// --- /RPC METHODS ---
2019-12-24 16:49:47 +02:00
// --- SSL Server support ---
2019-12-29 22:57:04 +02:00
ServerSSL : : ServerSSL ( const QSslCertificate & cert , const QSslKey & key , const QHostAddress & address_ , quint16 port_ ,
2019-12-30 10:33:35 +02:00
const std : : shared_ptr < Storage > & storage_ , const std : : shared_ptr < BitcoinDMgr > & bitcoindmgr_ )
: Server ( address_ , port_ , storage_ , bitcoindmgr_ ) , cert ( cert ) , key ( key )
2019-12-24 16:49:47 +02:00
{
if ( cert . isNull ( ) | | key . isNull ( ) )
throw BadArgs ( " ServerSSL cannot be instantiated: Key or cert are null! " ) ;
if ( ! QSslSocket : : supportsSsl ( ) )
throw BadArgs ( " ServerSSL cannot be instantiated: Missing SSL support! " ) ;
connect ( this , & ServerSSL : : ready , this , [ ] {
Debug ( ) < < " SSL ready emitted " ;
} ) ;
setObjectName ( prettyName ( ) ) ;
_thread . setObjectName ( prettyName ( ) ) ;
}
ServerSSL : : ~ ServerSSL ( ) { }
QString ServerSSL : : prettyName ( ) const
{
return QString ( " Ssl%1 " ) . arg ( AbstractTcpServer : : prettyName ( ) ) ;
}
void ServerSSL : : incomingConnection ( qintptr socketDescriptor )
{
QSslSocket * socket = new QSslSocket ( this ) ;
if ( socket - > setSocketDescriptor ( socketDescriptor ) ) {
socket - > setLocalCertificate ( cert ) ;
socket - > setPrivateKey ( key ) ;
socket - > setProtocol ( QSsl : : SslProtocol : : AnyProtocol ) ;
connect ( socket , & QSslSocket : : encrypted , this , & ServerSSL : : ready ) ;
connect ( socket , QOverload < const QList < QSslError > & > : : of ( & QSslSocket : : sslErrors ) ,
this , [ ] ( const QList < QSslError > & errors ) {
for ( auto e : errors )
Warning ( ) < < " SSL error: " < < e . errorString ( ) ;
} ) ;
addPendingConnection ( socket ) ;
socket - > startServerEncryption ( ) ;
} else {
Warning ( ) < < " setSocketDescriptor returned false -- unable to initiate SSL for client: " < < socket - > errorString ( ) ;
delete socket ;
}
}
// --- /SSL Server support ---
2019-05-03 17:28:51 +03:00
2019-12-29 11:04:47 +02:00
/*static*/ std : : atomic_int Client : : numClients { 0 } ;
2019-11-12 22:14:04 +02:00
Client : : Client ( const RPC : : MethodMap & mm , quint64 id_in , Server * srv , QTcpSocket * sock )
2019-11-12 00:12:05 +02:00
: RPC : : LinefeedConnection ( mm , id_in , sock , kMaxBuffer ) , srv ( srv )
2019-04-27 22:43:09 +03:00
{
2019-12-29 11:04:47 +02:00
const auto N = + + numClients ;
2019-04-28 14:59:01 +03:00
socket = sock ;
2019-12-18 17:53:16 +02:00
stale_threshold = 10 * 60 * 1000 ; // 10 mins stale threshold; after which clients get disconnected for being idle (for now... TODO: make this configurable)
pingtime_ms = int ( stale_threshold ) ; // this determines how often the pingtimer fires
2019-05-02 23:01:43 +03:00
Q_ASSERT ( socket - > state ( ) = = QAbstractSocket : : ConnectedState ) ;
status = Connected ; // we are always connected at construction time.
errorPolicy = ErrorPolicySendErrorMessage ;
2019-11-16 22:27:21 +02:00
setObjectName ( QString ( " Client.%1 " ) . arg ( id_in ) ) ;
2019-04-28 14:59:01 +03:00
on_connected ( ) ;
2019-12-29 11:04:47 +02:00
Log ( ) < < " New " < < prettyName ( false , false ) < < " , " < < N < < Util : : Pluralize ( " client " , N ) < < " total " ;
2019-04-27 22:43:09 +03:00
}
Client : : ~ Client ( )
{
2019-12-29 11:04:47 +02:00
- - numClients ;
2019-04-27 22:43:09 +03:00
Debug ( ) < < __PRETTY_FUNCTION__ ;
2019-04-28 14:59:01 +03:00
socket = nullptr ; // NB: we are a child of socket. this line here is added in case some day I make AbstractClient delete socket on destruct.
}
2019-04-30 19:21:47 +03:00
void Client : : do_disconnect ( bool graceful )
2019-04-28 14:59:01 +03:00
{
2019-04-30 13:31:56 +03:00
const bool wasConnected = socket ? socket - > state ( ) = = QAbstractSocket : : ConnectedState : false ;
2019-04-30 19:21:47 +03:00
AbstractConnection : : do_disconnect ( graceful ) ; // if 'graceful' *AND* was connected, a disconnected state will be entered later at which point we will delete socket.
2019-04-30 13:31:56 +03:00
if ( socket & & ( ! graceful | | ! wasConnected ) )
/// delete the socket if we weren't connected or if !graceful.
/// If graceful && connected, then a disconnect signal will be sent later and then we will
/// reenter here and delete the socket.
2019-04-30 13:32:16 +03:00
socket - > deleteLater ( ) ; // side-effect: will implicitly delete 'this' because we are a child of the socket!
2019-04-30 13:43:34 +03:00
else if ( socket & & graceful )
Debug ( ) < < __FUNCTION__ < < " (graceful); delayed socket delete (wait for disconnect) ... " ;
2019-04-28 14:59:01 +03:00
}
void Client : : do_ping ( )
{
2019-11-09 14:17:32 +02:00
// Don't send clients pings.
2019-05-06 17:31:52 +03:00
// Instead, rely on them to ping us else disconnect them if idle for too long.
// The below just checks idle.
2019-11-09 14:17:32 +02:00
if ( Util : : getTime ( ) - lastGood > = stale_threshold ) {
Debug ( ) < < prettyName ( ) < < " : idle timeout after " < < ( ( stale_threshold ) / 1e3 ) < < " sec., will close connection " ;
emit sendError ( true , RPC : : Code_Custom + 1 , " Idle time exceeded " ) ;
2019-05-02 23:01:43 +03:00
return ;
}
2019-04-27 22:43:09 +03:00
}