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