2019-07-17 13:37:18 -04:00
# include <unistd.h>
2020-03-01 04:34:08 -05:00
# include <csignal>
2019-07-17 13:37:18 -04:00
# include <sys/socket.h>
# include <netinet/in.h>
# include <algorithm>
2020-02-06 07:49:39 -05:00
# include <utility>
2019-07-17 13:37:18 -04:00
# include <arpa/inet.h>
# include <log/LogUtils.h>
# include <misc/endianness.h>
# include <LicenseRequest.pb.h>
2020-03-02 07:33:57 -05:00
# include <shared/include/license/license.h>
# include <shared/src/crypt.h>
2020-02-06 07:49:39 -05:00
# include <ThreadPool/ThreadHelper.h>
2019-07-17 13:37:18 -04:00
# include "LicenseServer.h"
# include "UserManager.h"
using namespace std ;
using namespace std : : chrono ;
using namespace license ;
using namespace ts ;
LicenseServer : : LicenseServer ( const sockaddr_in & addr ,
2020-02-28 05:24:07 -05:00
std : : shared_ptr < server : : database : : DatabaseHandler > manager ,
2020-02-06 07:49:39 -05:00
shared_ptr < license : : stats : : StatisticManager > stats ,
shared_ptr < license : : web : : WebStatistics > wstats ,
std : : shared_ptr < UserManager > user_manager ) : manager { std : : move ( manager ) } , statistics { std : : move ( stats ) } , web_statistics { std : : move ( wstats ) } , user_manager { std : : move ( user_manager ) } {
memcpy ( & this - > localAddr , & addr , sizeof ( addr ) ) ;
2019-07-17 13:37:18 -04:00
}
LicenseServer : : ~ LicenseServer ( ) {
2020-02-06 07:49:39 -05:00
this - > stop ( ) ;
2019-07-17 13:37:18 -04:00
}
# define SFAIL(message) \
do { \
2020-02-06 07:49:39 -05:00
logError ( LOG_GENERAL , " Message: {} ({}/{}) " , message , errno , strerror ( errno ) ) ; \
this - > stop ( ) ; \
2019-07-17 13:37:18 -04:00
return false ; \
} while ( 0 )
static int enabled = 1 ;
static int disabled = 0 ;
2020-02-06 07:49:39 -05:00
bool LicenseServer : : start ( ) {
2019-07-17 13:37:18 -04:00
{
2020-02-06 07:49:39 -05:00
lock_guard lock ( this - > client_lock ) ;
2019-07-17 13:37:18 -04:00
if ( this - > running ) return false ;
this - > running = true ;
}
2020-02-06 07:49:39 -05:00
server_socket = socket ( AF_INET , SOCK_STREAM , 0 ) ;
if ( server_socket < 0 ) SFAIL ( " Could not create new socket " ) ;
2019-07-17 13:37:18 -04:00
2020-02-06 07:49:39 -05:00
if ( setsockopt ( this - > server_socket , SOL_SOCKET , SO_REUSEADDR , & enabled , sizeof ( enabled ) ) < 0 ) SFAIL ( " could not set reuse address " ) ;
if ( setsockopt ( this - > server_socket , IPPROTO_TCP , TCP_CORK , & disabled , sizeof ( disabled ) ) < 0 ) SFAIL ( " could not set no push " ) ;
if ( bind ( this - > server_socket , ( struct sockaddr * ) & this - > localAddr , sizeof ( sockaddr_in ) ) < 0 ) SFAIL ( " Could not bind socket on " + string ( inet_ntoa ( this - > localAddr . sin_addr ) ) ) ;
2019-07-17 13:37:18 -04:00
2020-02-06 07:49:39 -05:00
if ( listen ( this - > server_socket , 32 ) < 0 ) SFAIL ( " Could not listen on socket " ) ;
2019-07-17 13:37:18 -04:00
this - > evBase = event_base_new ( ) ;
2020-02-06 07:49:39 -05:00
this - > event_accept = event_new ( this - > evBase , this - > server_socket , EV_READ | EV_PERSIST , LicenseServer : : handleEventAccept , this ) ;
this - > event_cleanup = evtimer_new ( this - > evBase , LicenseServer : : handleEventCleanup , this ) ;
2019-07-17 13:37:18 -04:00
2020-02-06 07:49:39 -05:00
event_add ( this - > event_accept , nullptr ) ;
2019-07-17 13:37:18 -04:00
{
timeval now { 1 , 0 } ;
2020-02-06 07:49:39 -05:00
evtimer_add ( this - > event_cleanup , & now ) ;
2019-07-17 13:37:18 -04:00
}
2020-02-06 07:49:39 -05:00
event_base_dispatch = std : : thread ( [ & ] {
2019-07-17 13:37:18 -04:00
signal ( SIGPIPE , SIG_IGN ) ;
2020-02-06 07:49:39 -05:00
: : event_base_dispatch ( this - > evBase ) ;
2019-07-17 13:37:18 -04:00
} ) ;
return true ;
}
2020-02-06 07:49:39 -05:00
void LicenseServer : : stop ( ) {
2019-07-17 13:37:18 -04:00
{
2020-02-06 07:49:39 -05:00
lock_guard lock ( this - > client_lock ) ;
2019-07-17 13:37:18 -04:00
if ( ! this - > running ) return ;
this - > running = false ;
}
2020-02-06 07:49:39 -05:00
/* first unregister the accept event so we don't get new clients */
if ( this - > event_accept ) {
event_del ( this - > event_accept ) ;
event_free ( this - > event_accept ) ;
2019-07-17 13:37:18 -04:00
}
2020-02-06 07:49:39 -05:00
this - > event_accept = nullptr ;
2019-07-17 13:37:18 -04:00
2020-02-06 07:49:39 -05:00
/* disconnect all clients */
for ( const auto & client : this - > getClients ( ) )
this - > closeConnection ( client ) ;
2019-07-17 13:37:18 -04:00
if ( this - > evBase )
event_base_loopbreak ( this - > evBase ) ;
2020-02-06 07:49:39 -05:00
if ( ! threads : : timed_join ( this - > event_base_dispatch , std : : chrono : : seconds { 2 } ) ) {
this - > event_base_dispatch . detach ( ) ;
logCritical ( LOG_GENERAL , " Failed to join event base dispatch thread. This will cause memory leaks. " ) ;
}
/* Needs to be cleaned up after event loop has been destroyed. Because its used within the event loop. */
if ( this - > event_cleanup ) {
event_del_block ( this - > event_cleanup ) ;
event_free ( this - > event_cleanup ) ;
this - > event_cleanup = nullptr ;
}
if ( this - > evBase )
2019-07-17 13:37:18 -04:00
event_base_free ( this - > evBase ) ;
this - > evBase = nullptr ;
2020-02-06 07:49:39 -05:00
if ( this - > server_socket ! = 0 ) {
shutdown ( this - > server_socket , SHUT_RDWR ) ;
close ( this - > server_socket ) ;
this - > server_socket = 0 ;
2019-07-17 13:37:18 -04:00
}
}
void LicenseServer : : handleEventCleanup ( int , short , void * ptrServer ) {
auto server = static_cast < LicenseServer * > ( ptrServer ) ;
server - > cleanup_clients ( ) ;
timeval next { 1 , 0 } ;
2020-02-06 07:49:39 -05:00
if ( server - > event_cleanup )
event_add ( server - > event_cleanup , & next ) ;
2019-07-17 13:37:18 -04:00
}
//Basic IO
void LicenseServer : : handleEventWrite ( int fd , short , void * ptrServer ) {
auto server = static_cast < LicenseServer * > ( ptrServer ) ;
auto client = server - > findClient ( fd ) ;
if ( ! client ) return ;
2020-02-06 07:49:39 -05:00
buffer : : RawBuffer * write_buffer { nullptr } ;
2020-05-01 04:48:29 -04:00
std : : unique_lock write_lock ( client - > network . write_queue_lock ) ;
2020-02-06 07:49:39 -05:00
while ( true ) { //TODO: May add some kind of timeout?
write_buffer = TAILQ_FIRST ( & client - > network . write_queue ) ;
if ( ! write_buffer ) return ;
auto writtenBytes = send ( fd , & write_buffer - > buffer [ write_buffer - > index ] , write_buffer - > length - write_buffer - > index , 0 ) ;
2019-11-22 14:51:00 -05:00
if ( writtenBytes < = 0 ) {
2020-05-01 04:48:29 -04:00
write_lock . unlock ( ) ;
2019-11-22 14:51:00 -05:00
if ( writtenBytes = = - 1 & & errno = = EAGAIN )
return ;
logError ( LOG_LICENSE_CONTROLL , " Invalid write. Disconnecting remote client. Message: {}/{} " , errno , strerror ( errno ) ) ;
2020-04-26 05:29:20 -04:00
server - > unregisterClient ( client ) ;
return ;
2019-11-22 14:51:00 -05:00
} else {
2020-02-06 07:49:39 -05:00
write_buffer - > index + = writtenBytes ;
2019-11-22 14:51:00 -05:00
}
2019-07-17 13:37:18 -04:00
2020-02-06 07:49:39 -05:00
if ( write_buffer - > index > = write_buffer - > length ) {
TAILQ_REMOVE ( & client - > network . write_queue , write_buffer , tail ) ;
delete write_buffer ;
2019-07-17 13:37:18 -04:00
}
2020-02-06 07:49:39 -05:00
if ( ! TAILQ_EMPTY ( & client - > network . write_queue ) )
2019-07-17 13:37:18 -04:00
event_add ( client - > network . writeEvent , nullptr ) ;
}
}
void ConnectedClient : : sendPacket ( const protocol : : packet & packet ) {
packet . prepare ( ) ;
auto buffer = new buffer : : RawBuffer ( packet . data . length ( ) + sizeof ( packet . header ) ) ;
memcpy ( buffer - > buffer , & packet . header , sizeof ( packet . header ) ) ;
memcpy ( & buffer - > buffer [ sizeof ( packet . header ) ] , packet . data . data ( ) , packet . data . length ( ) ) ;
if ( ! this - > protocol . cryptKey . empty ( ) )
xorBuffer ( & buffer - > buffer [ sizeof ( packet . header ) ] , packet . data . length ( ) , this - > protocol . cryptKey . data ( ) , this - > protocol . cryptKey . length ( ) ) ;
{
2020-02-06 07:49:39 -05:00
lock_guard queue_lock { this - > network . write_queue_lock } ;
TAILQ_INSERT_TAIL ( & this - > network . write_queue , buffer , tail ) ;
}
{
lock_guard state_lock { this - > protocol . state_lock } ;
if ( this - > protocol . state = = protocol : : UNCONNECTED ) goto error_cleanup ;
event_add ( this - > network . writeEvent , nullptr ) ;
return ;
2019-07-17 13:37:18 -04:00
}
2020-02-06 07:49:39 -05:00
error_cleanup :
delete buffer ;
2019-07-17 13:37:18 -04:00
}
void ConnectedClient : : init ( ) {
protocol . last_read = std : : chrono : : system_clock : : now ( ) ;
2020-02-06 07:49:39 -05:00
TAILQ_INIT ( & network . write_queue ) ;
2019-07-17 13:37:18 -04:00
}
2020-04-26 05:29:20 -04:00
void ConnectedClient : : uninit ( bool blocking ) {
2019-07-17 13:37:18 -04:00
{
2020-02-06 07:49:39 -05:00
lock_guard queue_lock { this - > network . write_queue_lock } ;
2019-07-17 13:37:18 -04:00
ts : : buffer : : RawBuffer * buffer ;
2020-02-06 07:49:39 -05:00
while ( ( buffer = TAILQ_FIRST ( & this - > network . write_queue ) ) ) {
TAILQ_REMOVE ( & this - > network . write_queue , buffer , tail ) ;
2019-07-17 13:37:18 -04:00
delete buffer ;
}
}
if ( network . fileDescriptor > 0 ) {
shutdown ( this - > network . fileDescriptor , SHUT_RDWR ) ;
close ( this - > network . fileDescriptor ) ;
network . fileDescriptor = 0 ;
}
2020-04-26 05:41:35 -04:00
std : : unique_lock elock { this - > network . event_mutex } ;
2020-04-26 05:29:20 -04:00
auto read_event = std : : exchange ( this - > network . readEvent , nullptr ) ;
auto write_event = std : : exchange ( this - > network . writeEvent , nullptr ) ;
2020-04-26 05:41:35 -04:00
elock . unlock ( ) ;
2020-04-26 05:29:20 -04:00
if ( blocking ) {
if ( read_event ) event_del_block ( read_event ) ;
if ( write_event ) event_del_block ( write_event ) ;
} else {
if ( read_event ) event_del_noblock ( read_event ) ;
if ( write_event ) event_del_noblock ( write_event ) ;
}
2020-04-26 05:41:35 -04:00
if ( read_event ) event_free ( read_event ) ;
if ( write_event ) event_free ( write_event ) ;
2019-07-17 13:37:18 -04:00
}
void LicenseServer : : handleEventRead ( int fd , short , void * ptrServer ) {
auto server = static_cast < LicenseServer * > ( ptrServer ) ;
auto client = server - > findClient ( fd ) ;
if ( ! client ) return ;
char buffer [ 1024 ] ;
sockaddr_in remoteAddress { } ;
socklen_t remoteAddressSize = sizeof ( remoteAddress ) ;
auto read = recvfrom ( fd , buffer , 1024 , 0 , reinterpret_cast < sockaddr * > ( & remoteAddress ) , & remoteAddressSize ) ;
if ( read < 0 ) {
if ( errno = = EWOULDBLOCK ) return ;
2019-11-22 14:51:00 -05:00
logError ( LOG_LICENSE_CONTROLL , " Invalid read. Disconnecting remote client. Message: {}/{} " , errno , strerror ( errno ) ) ;
2020-04-26 05:41:35 -04:00
{
std : : lock_guard elock { client - > network . event_mutex } ;
if ( client - > network . readEvent )
event_del_noblock ( client - > network . readEvent ) ;
}
2019-07-17 13:37:18 -04:00
server - > closeConnection ( client ) ;
return ;
} else if ( read = = 0 ) {
2020-02-28 05:24:07 -05:00
logMessage ( LOG_LICENSE_CONTROLL , " [CLIENT][ " + client - > address ( ) + " ] Received EOF for client. Removing client. " ) ;
2020-04-26 05:41:35 -04:00
{
std : : lock_guard elock { client - > network . event_mutex } ;
if ( client - > network . readEvent )
event_del_noblock ( client - > network . readEvent ) ;
}
2019-07-17 13:37:18 -04:00
server - > closeConnection ( client ) ;
return ;
}
client - > protocol . last_read = std : : chrono : : system_clock : : now ( ) ;
server - > handleMessage ( client , string ( buffer , read ) ) ;
}
void LicenseServer : : handleEventAccept ( int fd , short , void * ptrServer ) {
auto server = static_cast < LicenseServer * > ( ptrServer ) ;
auto client = make_shared < ConnectedClient > ( ) ;
client - > init ( ) ;
socklen_t client_len = sizeof ( client - > network . remoteAddr ) ;
client - > network . fileDescriptor = accept ( fd , ( struct sockaddr * ) & client - > network . remoteAddr , & client_len ) ;
if ( setsockopt ( client - > network . fileDescriptor , SOL_SOCKET , SO_REUSEADDR , & enabled , sizeof ( enabled ) ) < 0 ) ; // CERR("could not set reuse addr");
if ( setsockopt ( client - > network . fileDescriptor , IPPROTO_TCP , TCP_CORK , & disabled , sizeof ( disabled ) ) < 0 ) ; // CERR("could not set no push");
if ( client - > network . fileDescriptor < 0 ) {
2020-02-06 07:49:39 -05:00
logCritical ( LOG_GENERAL , " Could not accept new client! ( " + to_string ( client - > network . fileDescriptor ) + " | " + to_string ( errno ) + " | " + strerror ( errno ) + " ) " ) ;
2019-07-17 13:37:18 -04:00
return ;
}
2020-02-28 05:24:07 -05:00
client - > protocol . state = protocol : : HANDSCHAKE ;
2019-07-17 13:37:18 -04:00
{
2020-02-06 07:49:39 -05:00
lock_guard lock ( server - > client_lock ) ;
server - > clients . push_back ( client ) ;
2019-07-17 13:37:18 -04:00
}
client - > network . readEvent = event_new ( server - > evBase , client - > network . fileDescriptor , EV_READ | EV_PERSIST , LicenseServer : : handleEventRead , server ) ;
client - > network . writeEvent = event_new ( server - > evBase , client - > network . fileDescriptor , EV_WRITE , LicenseServer : : handleEventWrite , server ) ;
event_add ( client - > network . readEvent , nullptr ) ;
2020-02-06 07:49:39 -05:00
logMessage ( LOG_GENERAL , " Accepted new client from {} " , inet_ntoa ( client - > network . remoteAddr . sin_addr ) ) ;
2019-07-17 13:37:18 -04:00
}
void LicenseServer : : disconnectClient ( const std : : shared_ptr < ConnectedClient > & client , const std : : string & reason ) {
client - > sendPacket ( { protocol : : PACKET_DISCONNECT , reason } ) ;
}
void LicenseServer : : closeConnection ( const std : : shared_ptr < ConnectedClient > & client , bool blocking ) {
2020-02-06 07:49:39 -05:00
if ( this_thread : : get_id ( ) = = this - > event_base_dispatch . get_id ( ) ) {
2019-07-17 13:37:18 -04:00
std : : thread ( std : : bind ( & LicenseServer : : closeConnection , this , client , true ) ) . detach ( ) ;
return ;
}
{
2020-02-06 07:49:39 -05:00
unique_lock lock ( client - > network . write_queue_lock ) ;
if ( ! TAILQ_EMPTY ( & client - > network . write_queue ) ) {
2019-07-17 13:37:18 -04:00
lock . unlock ( ) ;
if ( ! blocking ) {
std : : thread ( std : : bind ( & LicenseServer : : closeConnection , this , client , true ) ) . detach ( ) ;
return ;
}
auto start = system_clock : : now ( ) ;
while ( system_clock : : now ( ) - start < seconds ( 5 ) ) {
{
lock . lock ( ) ;
2020-02-06 07:49:39 -05:00
if ( TAILQ_EMPTY ( & client - > network . write_queue ) ) break ;
2019-07-17 13:37:18 -04:00
lock . unlock ( ) ;
}
threads : : self : : sleep_for ( milliseconds ( 5 ) ) ;
}
}
}
this - > unregisterClient ( client ) ;
}
void LicenseServer : : unregisterClient ( const std : : shared_ptr < ConnectedClient > & client ) {
{
2020-02-06 07:49:39 -05:00
lock_guard lock ( this - > client_lock ) ;
2019-07-17 13:37:18 -04:00
2020-02-06 07:49:39 -05:00
auto it = find ( this - > clients . begin ( ) , this - > clients . end ( ) , client ) ;
if ( it ! = this - > clients . end ( ) )
this - > clients . erase ( it ) ;
2019-07-17 13:37:18 -04:00
}
2020-02-06 07:49:39 -05:00
{
std : : lock_guard state_lock { client - > protocol . state_lock } ;
client - > protocol . state = protocol : : UNCONNECTED ;
}
2020-04-26 05:29:20 -04:00
client - > uninit ( this_thread : : get_id ( ) ! = this - > event_base_dispatch . get_id ( ) ) ;
2019-07-17 13:37:18 -04:00
}
void LicenseServer : : cleanup_clients ( ) {
2020-02-06 07:49:39 -05:00
unique_lock lock ( this - > client_lock ) ;
auto clients = this - > clients ;
2019-07-17 13:37:18 -04:00
lock . unlock ( ) ;
2020-02-06 07:49:39 -05:00
size_t cleanup_count { 0 } ;
2019-07-17 13:37:18 -04:00
for ( const auto & client : clients ) {
if ( client - > protocol . last_read + minutes ( 1 ) < system_clock : : now ( ) ) {
2020-02-06 07:49:39 -05:00
cleanup_count + + ;
2019-07-17 13:37:18 -04:00
if ( client - > protocol . state ! = protocol : : DISCONNECTING & & client - > protocol . state ! = protocol : : UNCONNECTED ) {
this - > disconnectClient ( client , " timeout " ) ;
this - > closeConnection ( client ) ;
2020-02-06 07:49:39 -05:00
std : : lock_guard state_lock { client - > protocol . state_lock } ;
client - > protocol . state = protocol : : UNCONNECTED ;
2019-07-17 13:37:18 -04:00
} else {
2020-02-06 07:49:39 -05:00
auto it = find ( this - > clients . begin ( ) , this - > clients . end ( ) , client ) ;
if ( it ! = this - > clients . end ( ) )
this - > clients . erase ( it ) ;
2019-07-17 13:37:18 -04:00
}
}
}
2020-02-06 07:49:39 -05:00
if ( cleanup_count )
debugMessage ( LOG_GENERAL , " {} clients have been cleaned up due to a read timeout. " , cleanup_count ) ;
2019-07-17 13:37:18 -04:00
}
2020-02-06 07:49:39 -05:00
std : : shared_ptr < ConnectedClient > LicenseServer : : findClient ( int fd ) {
lock_guard lock ( this - > client_lock ) ;
for ( const auto & cl : this - > clients )
if ( cl - > network . fileDescriptor = = fd )
2019-07-17 13:37:18 -04:00
return cl ;
return nullptr ;
}
# define ERR(message) \
do { \
2020-02-06 07:49:39 -05:00
logError ( LOG_GENERAL , message ) ; \
2019-07-17 13:37:18 -04:00
this - > closeConnection ( client ) ; \
return ; \
} while ( 0 )
void LicenseServer : : handleMessage ( shared_ptr < ConnectedClient > & client , const std : : string & message ) {
if ( message . length ( ) < sizeof ( protocol : : packet : : header ) ) ERR ( " A client tried to send a invalid packet (too small header) " ) ;
protocol : : packet packet { protocol : : PACKET_DISCONNECT , " " } ;
memcpy ( & packet . header , message . data ( ) , sizeof ( protocol : : packet : : header ) ) ;
packet . data = message . substr ( sizeof ( protocol : : packet : : header ) , packet . header . length ) ;
2020-03-02 07:33:57 -05:00
if ( ! client - > protocol . cryptKey . empty ( ) ) {
2019-07-17 13:37:18 -04:00
xorBuffer ( ( char * ) packet . data . data ( ) , packet . data . length ( ) , client - > protocol . cryptKey . data ( ) , client - > protocol . cryptKey . length ( ) ) ;
2020-03-02 07:33:57 -05:00
}
2019-07-17 13:37:18 -04:00
2020-03-02 07:33:57 -05:00
bool success { false } ;
2019-07-17 13:37:18 -04:00
string error ;
try {
if ( packet . header . packetId = = protocol : : PACKET_CLIENT_HANDSHAKE ) {
success = this - > handleHandshake ( client , packet , error ) ;
} else if ( packet . header . packetId = = protocol : : PACKET_DISCONNECT ) {
success = this - > handleDisconnect ( client , packet , error ) ;
} else if ( packet . header . packetId = = protocol : : PACKET_CLIENT_SERVER_VALIDATION ) {
success = this - > handleServerValidation ( client , packet , error ) ;
} else if ( packet . header . packetId = = protocol : : PACKET_CLIENT_PROPERTY_ADJUSTMENT ) {
success = this - > handlePacketPropertyUpdate ( client , packet , error ) ;
2020-02-28 05:24:07 -05:00
} else if ( packet . header . packetId = = protocol : : PACKET_CLIENT_LICENSE_UPGRADE ) {
success = this - > handlePacketLicenseUpgrade ( client , packet , error ) ;
2019-07-17 13:37:18 -04:00
} else if ( packet . header . packetId = = protocol : : PACKET_CLIENT_AUTH_REQUEST ) {
success = this - > handlePacketAuth ( client , packet , error ) ;
} else if ( packet . header . packetId = = protocol : : PACKET_CLIENT_LICENSE_CREATE_REQUEST ) {
success = this - > handlePacketLicenseCreate ( client , packet , error ) ;
} else if ( packet . header . packetId = = protocol : : PACKET_CLIENT_LIST_REQUEST ) {
success = this - > handlePacketLicenseList ( client , packet , error ) ;
} else if ( packet . header . packetId = = protocol : : PACKET_CLIENT_DELETE_REQUEST ) {
success = this - > handlePacketLicenseDelete ( client , packet , error ) ;
} else if ( packet . header . packetId = = protocol : : PACKET_PING ) {
/* nothing todo */
} else error = " Invalid packet id! " ;
} catch ( std : : exception & ex ) {
success = false ;
error = " Caught exception while handle packet: " + string ( ex . what ( ) ) ;
}
if ( ! success ) {
2020-02-06 07:49:39 -05:00
logError ( LOG_GENERAL , " [CLIENT][ " + client - > address ( ) + " ] Failed to handle packet. message: " + error ) ;
2019-07-17 13:37:18 -04:00
this - > disconnectClient ( client , error ) ;
}
}