E-MailRelay
gclient.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2021 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gclient.cpp
19///
20
21#include "gdef.h"
22#include "gaddress.h"
23#include "gsocket.h"
24#include "gdatetime.h"
25#include "gexception.h"
26#include "gresolver.h"
27#include "groot.h"
28#include "gmonitor.h"
29#include "gclient.h"
30#include "gassert.h"
31#include "gtest.h"
32#include "glog.h"
33#include <numeric> // std::accumulate
34#include <sstream>
35#include <cstdlib>
36
37GNet::Client::Client( ExceptionSink es , const Location & remote , const Config & config ) :
38 m_es(es) ,
39 m_line_buffer(config.line_buffer_config) ,
40 m_remote_location(remote) ,
41 m_bind_local_address(config.bind_local_address) ,
42 m_local_address(config.local_address) ,
43 m_sync_dns(config.sync_dns) ,
44 m_secure_connection_timeout(config.secure_connection_timeout) ,
45 m_connection_timeout(config.connection_timeout) ,
46 m_response_timeout(config.response_timeout) ,
47 m_idle_timeout(config.idle_timeout) ,
48 m_state(State::Idle) ,
49 m_finished(false) ,
50 m_has_connected(false) ,
51 m_start_timer(*this,&GNet::Client::onStartTimeout,es) ,
52 m_connect_timer(*this,&GNet::Client::onConnectTimeout,es) ,
53 m_connected_timer(*this,&GNet::Client::onConnectedTimeout,es) ,
54 m_response_timer(*this,&GNet::Client::onResponseTimeout,es) ,
55 m_idle_timer(*this,&GNet::Client::onIdleTimeout,es)
56{
57 G_DEBUG( "Client::ctor" ) ;
58 if( config.auto_start )
59 m_start_timer.startTimer( 0U ) ;
60 Monitor::addClient( *this ) ;
61}
62
64{
65 Monitor::removeClient( *this ) ;
66}
67
69{
70 G_DEBUG( "GNet::Client::disconnect" ) ;
71
72 m_start_timer.cancelTimer() ;
73 m_connect_timer.cancelTimer() ;
74 m_connected_timer.cancelTimer() ;
75 m_response_timer.cancelTimer() ;
76 m_idle_timer.cancelTimer() ;
77
78 m_state = State::Disconnected ;
79 m_finished = true ;
80
81 m_sp.reset() ;
82 m_socket.reset() ;
83 m_resolver.reset() ;
84}
85
87{
88 return m_event_signal ;
89}
90
92{
93 return m_remote_location ;
94}
95
97{
98 if( m_socket == nullptr )
99 throw NotConnected() ;
100 return *m_socket ;
101}
102
104{
105 if( m_socket == nullptr )
106 throw NotConnected() ;
107 return *m_socket ;
108}
109
111{
112 m_line_buffer.clear() ;
113 m_response_timer.cancelTimer() ;
114}
115
116void GNet::Client::onStartTimeout()
117{
118 G_DEBUG( "GNet::Client::onStartTimeout: auto-start connecting" ) ;
119 connect() ;
120}
121
123{
124 G_DEBUG( "GNet::Client::connect: [" << m_remote_location.displayString() << "] "
125 << "(" << static_cast<int>(m_state) << ")" ) ;
126 if( m_state != State::Idle )
127 throw ConnectError( "wrong state" ) ;
128
129 // (one timer covers dns resolution and socket connection)
130 if( m_connection_timeout )
131 m_connect_timer.startTimer( m_connection_timeout ) ;
132
133 m_remote_location.resolveTrivially() ; // if host:service is already address:port
134 if( m_remote_location.resolved() )
135 {
136 setState( State::Connecting ) ;
137 startConnecting() ;
138 }
139 else if( m_sync_dns || !Resolver::async() )
140 {
141 std::string error = Resolver::resolve( m_remote_location ) ;
142 if( !error.empty() )
143 throw DnsError( error ) ;
144
145 setState( State::Connecting ) ;
146 startConnecting() ;
147 }
148 else
149 {
150 setState( State::Resolving ) ;
151 if( m_resolver == nullptr )
152 {
153 Resolver::Callback & resolver_callback = *this ;
154 m_resolver = std::make_unique<Resolver>( resolver_callback , m_es ) ;
155 }
156 m_resolver->start( m_remote_location ) ;
157 emit( "resolving" ) ;
158 }
159}
160
161void GNet::Client::onResolved( std::string error , Location location )
162{
163 if( !error.empty() )
164 throw DnsError( error ) ;
165
166 G_DEBUG( "GNet::Client::onResolved: " << location.displayString() ) ;
167 m_remote_location.update( location.address() , location.name() ) ;
168 setState( State::Connecting ) ;
169 startConnecting() ;
170}
171
172void GNet::Client::startConnecting()
173{
174 G_DEBUG( "GNet::Client::startConnecting: local: " << m_local_address.displayString() ) ;
175 G_DEBUG( "GNet::Client::startConnecting: remote: " << m_remote_location.displayString() ) ;
176 if( G::Test::enabled("client-slow-connect") )
177 setState( State::Testing ) ;
178
179 // create and open a socket
180 //
181 m_sp.reset() ;
182 m_socket = std::make_unique<StreamSocket>( m_remote_location.address().family() ) ;
183 socket().addWriteHandler( *this , m_es ) ;
184
185 // create a socket protocol object
186 //
187 EventHandler & eh = *this ;
188 SocketProtocolSink & sp_sink = *this ;
189 m_sp = std::make_unique<SocketProtocol>( eh , m_es , sp_sink , *m_socket , m_secure_connection_timeout ) ;
190
191 // bind a local address to the socket (throws on failure)
192 //
193 if( m_bind_local_address )
194 bindLocalAddress( m_local_address ) ;
195
196 // start connecting
197 //
198 bool immediate = false ;
199 if( !socket().connect( m_remote_location.address() , &immediate ) )
200 throw ConnectError( "cannot connect to " + m_remote_location.address().displayString() ) ;
201
202 // deal with immediate connection (typically if connecting locally)
203 //
204 if( immediate )
205 {
206 socket().dropWriteHandler() ;
207 m_connected_timer.startTimer( 0U ) ; // -> onConnectedTimeout()
208 }
209 else
210 {
211 emit( "connecting" ) ;
212 }
213}
214
215void GNet::Client::finish( bool with_socket_shutdown )
216{
217 m_finished = true ;
218 if( with_socket_shutdown )
219 {
220 if( m_sp != nullptr )
221 m_sp->shutdown() ;
222 else if( m_socket != nullptr )
223 m_socket->shutdown() ;
224 }
225}
226
228{
229 return m_finished ;
230}
231
233{
234 return m_has_connected ;
235}
236
237void GNet::Client::doOnDelete( const std::string & reason , bool done )
238{
239 onDelete( (done||m_finished) ? std::string() : reason ) ;
240}
241
242void GNet::Client::emit( const std::string & action )
243{
244 m_event_signal.emit( std::string(action) , m_remote_location.displayString() , std::string() ) ;
245}
246
247void GNet::Client::onConnectTimeout()
248{
249 std::ostringstream ss ;
250 ss << "cannot connect to " << m_remote_location << ": timed out out after " << m_connection_timeout << "s" ;
251 G_DEBUG( "GNet::Client::onConnectTimeout: " << ss.str() ) ;
252 throw ConnectError( ss.str() ) ;
253}
254
255void GNet::Client::onResponseTimeout()
256{
257 std::ostringstream ss ;
258 ss << "no response after " << m_response_timeout << "s while connected to " << m_remote_location ;
259 G_DEBUG( "GNet::Client::onResponseTimeout: response timeout: " << ss.str() ) ;
260 throw ResponseTimeout( ss.str() ) ;
261}
262
263void GNet::Client::onIdleTimeout()
264{
265 std::ostringstream ss ;
266 ss << "no activity after " << m_idle_timeout << "s while connected to " << m_remote_location ;
267 throw IdleTimeout( ss.str() ) ;
268}
269
270void GNet::Client::onConnectedTimeout()
271{
272 G_DEBUG( "GNet::Client::onConnectedTimeout: immediate connection" ) ;
273 onWriteable() ;
274}
275
276void GNet::Client::writeEvent()
277{
278 G_DEBUG( "GNet::Client::writeEvent" ) ;
279 onWriteable() ;
280}
281
282void GNet::Client::onWriteable()
283{
284 bool has_peer = m_state == State::Connecting && socket().getPeerAddress().first ;
285 if( m_state == State::Connected )
286 {
287 if( m_sp->writeEvent() )
288 onSendComplete() ;
289 }
290 else if( m_state == State::Testing )
291 {
292 socket().dropWriteHandler() ;
293 setState( State::Connecting ) ;
294 m_connected_timer.startTimer( 2U , 100000U ) ; // -> onConnectedTimeout()
295 }
296 else if( m_state == State::Connecting && has_peer && m_remote_location.socks() )
297 {
298 setState( State::Socksing ) ;
299 m_socks = std::make_unique<Socks>( m_remote_location ) ;
300 if( m_socks->send( socket() ) )
301 {
302 socket().addOtherHandler( *this , m_es ) ;
303 socket().dropWriteHandler() ;
304 socket().addReadHandler( *this , m_es ) ; // wait for the socks response
305 }
306 else
307 {
308 socket().addOtherHandler( *this , m_es ) ;
309 socket().addWriteHandler( *this , m_es ) ;
310 socket().dropReadHandler() ;
311 }
312 }
313 else if( m_state == State::Connecting && has_peer )
314 {
315 socket().dropWriteHandler() ;
316 socket().addReadHandler( *this , m_es ) ;
317 socket().addOtherHandler( *this , m_es ) ;
318
319 setState( State::Connected ) ;
320 doOnConnect() ;
321 }
322 else if( m_state == State::Connecting )
323 {
324 socket().dropWriteHandler() ;
325 throw ConnectError( "cannot connect to " + m_remote_location.address().displayString() ) ;
326 }
327 else if( m_state == State::Socksing )
328 {
329 G_ASSERT( m_socks != nullptr ) ;
330 if( m_socks->send( socket() ) )
331 {
332 socket().dropWriteHandler() ;
333 socket().addReadHandler( *this , m_es ) ;
334
335 setState( State::Connected ) ;
336 doOnConnect() ;
337 }
338 }
339 else if( m_state == State::Disconnected )
340 {
341 // never gets here
342 }
343}
344
345void GNet::Client::doOnConnect()
346{
347 G::CallFrame this_( m_call_stack ) ;
348 onConnect() ;
349 if( this_.deleted() ) return ;
350 emit( "connected" ) ;
351}
352
353void GNet::Client::otherEvent( EventHandler::Reason reason )
354{
355 if( m_state == State::Socksing || m_sp == nullptr )
356 EventHandler::otherEvent( reason ) ; // default implementation
357 else
358 m_sp->otherEvent( reason ) ;
359}
360
361void GNet::Client::readEvent()
362{
363 G_ASSERT( m_sp != nullptr ) ;
364 if( m_state == State::Socksing )
365 {
366 G_ASSERT( m_socks != nullptr ) ;
367 bool complete = m_socks->read( socket() ) ;
368 if( complete )
369 {
370 setState( State::Connected ) ;
371 doOnConnect() ;
372 }
373 }
374 else
375 {
376 if( m_sp != nullptr )
377 m_sp->readEvent() ;
378 }
379}
380
381void GNet::Client::onData( const char * data , std::size_t size )
382{
383 if( m_response_timeout && m_line_buffer.transparent() ) // anything will do
384 m_response_timer.cancelTimer() ;
385
386 if( m_idle_timeout )
387 m_idle_timer.startTimer( m_idle_timeout ) ;
388
389 bool fragments = m_line_buffer.transparent() ;
390 m_line_buffer.apply( this , &Client::onDataImp , data , size , fragments ) ;
391}
392
393bool GNet::Client::onDataImp( const char * data , std::size_t size , std::size_t eolsize ,
394 std::size_t linesize , char c0 )
395{
396 if( m_response_timeout && eolsize ) // end of a complete line
397 m_response_timer.cancelTimer() ;
398
399 return onReceive( data , size , eolsize , linesize , c0 ) ;
400}
401
403{
404 return m_state == State::Connected ;
405}
406
407void GNet::Client::bindLocalAddress( const Address & local_address )
408{
409 {
410 G::Root claim_root ;
411 socket().bind( local_address ) ;
412 }
413
414 if( local_address.isLoopback() && !m_remote_location.address().isLoopback() )
415 G_WARNING_ONCE( "GNet::Client::bindLocalAddress: binding the loopback address for "
416 "outgoing connections may result in connection failures" ) ;
417}
418
419void GNet::Client::setState( State new_state )
420{
421 if( new_state != State::Connecting && new_state != State::Resolving )
422 m_connect_timer.cancelTimer() ;
423
424 if( new_state == State::Connected )
425 m_has_connected = true ;
426
427 if( new_state == State::Connected && m_idle_timeout )
428 m_idle_timer.startTimer( m_idle_timeout ) ;
429
430 m_state = new_state ;
431}
432
433std::pair<bool,GNet::Address> GNet::Client::localAddress() const
434{
435 return
436 m_socket != nullptr ?
437 std::make_pair(true,socket().getLocalAddress()) :
438 std::make_pair(false,GNet::Address::defaultAddress()) ;
439}
440
441std::pair<bool,GNet::Address> GNet::Client::peerAddress() const
442{
443 return
444 m_socket != nullptr ?
445 socket().getPeerAddress() :
446 std::make_pair(false,GNet::Address::defaultAddress()) ;
447}
448
450{
451 std::pair<bool,Address> pair =
452 m_socket != nullptr ?
453 socket().getPeerAddress() :
454 std::make_pair(false,GNet::Address::defaultAddress()) ;
455
456 return
457 pair.first ?
458 pair.second.displayString() :
459 ("("+m_remote_location.displayString()+")") ;
460}
461
463{
464 return m_sp->peerCertificate() ;
465}
466
468{
469 if( m_sp == nullptr )
470 throw NotConnected( "for secure-connect" ) ;
471 m_sp->secureConnect() ;
472}
473
474bool GNet::Client::send( const std::string & data , std::size_t offset )
475{
476 if( m_response_timeout && data.size() > offset )
477 m_response_timer.startTimer( m_response_timeout ) ;
478 return m_sp->send( data , offset ) ;
479}
480
481bool GNet::Client::send( const std::vector<G::string_view> & data , std::size_t offset )
482{
483 std::size_t total_size = std::accumulate( data.begin() , data.end() , std::size_t(0U) ,
484 [](std::size_t n,G::string_view s){return n+s.size();} ) ;
485 if( m_response_timeout && offset < total_size )
486 m_response_timer.startTimer( m_response_timeout ) ;
487 return m_sp->send( data , offset ) ;
488}
489
491{
492 return m_line_buffer.state() ;
493}
494
495// ==
496
497namespace GNet
498{
499 namespace ClientImp
500 {
501 bool sync_default()
502 {
503 if( G::Test::enabled("client-dns-asynchronous") ) return false ;
504 if( G::Test::enabled("client-dns-synchronous") ) return true ;
505 return false ;
506 }
507 }
508}
509
510GNet::Client::Config::Config() :
511 local_address(Address::defaultAddress()) ,
512 line_buffer_config(LineBufferConfig::transparent()) ,
513 sync_dns(ClientImp::sync_default())
514{
515}
516
517GNet::Client::Config::Config( const LineBufferConfig & lbc ) :
518 local_address(Address::defaultAddress()) ,
519 line_buffer_config(lbc) ,
520 sync_dns(ClientImp::sync_default())
521{
522}
523
524GNet::Client::Config::Config( const LineBufferConfig & lbc , unsigned int connection_timeout_in ,
525 unsigned int secure_connection_timeout_in , unsigned int response_timeout_in ,
526 unsigned int idle_timeout_in ) :
527 local_address(Address::defaultAddress()) ,
528 line_buffer_config(lbc) ,
529 sync_dns(ClientImp::sync_default()) ,
530 connection_timeout(connection_timeout_in) ,
531 secure_connection_timeout(secure_connection_timeout_in) ,
532 response_timeout(response_timeout_in) ,
533 idle_timeout(idle_timeout_in)
534{
535}
536
537GNet::Client::Config::Config( const LineBufferConfig & lbc , unsigned int all_timeouts ) :
538 local_address(Address::defaultAddress()) ,
539 line_buffer_config(lbc) ,
540 sync_dns(ClientImp::sync_default()) ,
541 connection_timeout(all_timeouts) ,
542 secure_connection_timeout(all_timeouts) ,
543 response_timeout(all_timeouts) ,
544 idle_timeout(all_timeouts*2U)
545{
546}
547
548GNet::Client::Config & GNet::Client::Config::set_all_timeouts( unsigned int all_timeouts )
549{
550 connection_timeout = all_timeouts ;
551 secure_connection_timeout = all_timeouts ;
552 response_timeout = all_timeouts ;
553 idle_timeout = all_timeouts * 2U ;
554 return *this ;
555}
556
The GNet::Address class encapsulates a TCP/UDP transport address.
Definition: gaddress.h:53
static Address defaultAddress()
Returns a default address, being the IPv4 wildcard address with a zero port number.
Definition: gaddress.cpp:242
bool isLoopback() const
Returns true if this is a loopback address.
Definition: gaddress.cpp:288
A class for making an outgoing connection to a remote server, with support for socket-level protocols...
Definition: gclient.h:75
void disconnect()
Aborts the connection and destroys the object's internal state, resulting in a zombie object.
Definition: gclient.cpp:68
std::string peerCertificate() const override
Returns the peer's TLS certificate.
Definition: gclient.cpp:462
void secureConnect()
Starts TLS/SSL client-side negotiation.
Definition: gclient.cpp:467
~Client() override
Destructor.
Definition: gclient.cpp:63
bool send(const std::string &data, std::size_t offset=0)
Sends data to the peer and starts the response timer (if configured).
Definition: gclient.cpp:474
void finish(bool with_socket_shutdown)
Indicates that the last data has been sent and the client is expecting a peer disconnect.
Definition: gclient.cpp:215
G::Slot::Signal< const std::string &, const std::string &, const std::string & > & eventSignal() noexcept
Returns a signal that indicates that something interesting has happened.
Definition: gclient.cpp:86
StreamSocket & socket()
Returns a reference to the socket. Throws if not connected.
Definition: gclient.cpp:96
std::string connectionState() const override
Returns the connection state display string.
Definition: gclient.cpp:449
bool finished() const
Returns true if finish()ed or disconnect()ed.
Definition: gclient.cpp:227
LineBufferState lineBuffer() const
Returns information about the state of the internal line-buffer.
Definition: gclient.cpp:490
bool hasConnected() const
Returns true if ever connected().
Definition: gclient.cpp:232
bool connected() const
Returns true if connected to the peer.
Definition: gclient.cpp:402
Location remoteLocation() const
Returns a Location structure, including the result of name lookup if available.
Definition: gclient.cpp:91
Client(ExceptionSink, const Location &remote_location, const Config &)
Constructor.
Definition: gclient.cpp:37
void connect()
Initiates a connection to the remote server.
Definition: gclient.cpp:122
std::pair< bool, Address > peerAddress() const override
Override from Connection.
Definition: gclient.cpp:441
void doOnDelete(const std::string &reason, bool done)
Called by ClientPtr (or equivalent) to call onDelete(), just before this client object is deleted.
Definition: gclient.cpp:237
std::pair< bool, Address > localAddress() const override
Override from Connection.
Definition: gclient.cpp:433
void clearInput()
Clears the input LineBuffer and cancels the response timer if running.
Definition: gclient.cpp:110
virtual void otherEvent(Reason)
Called for a socket-exception event, or a socket-close event on windows.
A tuple containing an ExceptionHandler interface pointer and a bound 'exception source' pointer.
Provides information abount the state of a line buffer.
Definition: glinebuffer.h:383
A class that represents the remote target for out-going client connections.
Definition: glocation.h:71
std::string name() const
Returns the remote canonical name.
Definition: glocation.cpp:177
std::string displayString() const
Returns a string representation for logging and debug.
Definition: glocation.cpp:182
Address address() const
Returns the remote address.
Definition: glocation.cpp:147
static void removeClient(const Connection &client) noexcept
Removes a client connection.
Definition: gmonitor.cpp:117
static void addClient(const Connection &client)
Adds a client connection.
Definition: gmonitor.cpp:108
static std::string resolve(Location &)
Does synchronous name resolution.
Definition: gresolver.cpp:194
static bool async()
Returns true if the resolver supports asynchronous operation.
Definition: gresolver.cpp:255
A derivation of GNet::Socket for a stream socket.
Definition: gsocket.h:311
An object to represent a nested execution context.
Definition: gcall.h:87
A class which acquires the process's special privileges on construction and releases them on destruct...
Definition: groot.h:52
static bool enabled() noexcept
Returns true if test features are enabled.
Definition: gtest.cpp:79
A class template like c++17's std::basic_string_view.
Definition: gstringview.h:73
Network classes.
Definition: gdef.h:1115
A structure containing GNet::Client configuration parameters.
Definition: gclient.h:84
An interface used for GNet::Resolver callbacks.
Definition: gresolver.h:50