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