66__author__ = 'Simon Robinson'
77__copyright__ = 'Copyright (c) 2022 Simon Robinson'
88__license__ = 'Apache 2.0'
9- __version__ = '2023-04-03 ' # ISO 8601 (YYYY-MM-DD)
9+ __version__ = '2023-04-04 ' # ISO 8601 (YYYY-MM-DD)
1010
1111import abc
1212import argparse
@@ -274,6 +274,12 @@ def error(*args):
274274 def error_string (error ):
275275 return getattr (error , 'message' , repr (error ))
276276
277+ @staticmethod
278+ def get_last_error ():
279+ error_type , value , _traceback = sys .exc_info ()
280+ del _traceback # used to be required in python 2; may no-longer be needed, but best to be safe
281+ return error_type , value # note that if no exception has currently been raised, this will return `None, None`
282+
277283
278284class CacheStore (abc .ABC ):
279285 """Override this class to provide additional cache store options for a dictionary of OAuth 2.0 credentials, then add
@@ -1008,7 +1014,7 @@ def _ssl_handshake(self):
10081014 except ssl .SSLWantWriteError :
10091015 select .select ([], [self .socket ], [], 0.01 ) # wait for the socket to be writable (10ms timeout)
10101016 except self .ssl_handshake_errors : # also includes SSLWant[Read/Write]Error, but already handled above
1011- self .handle_close ()
1017+ self .close ()
10121018 else :
10131019 if not self .ssl_handshake_completed : # only notify once (we may need to repeat the handshake later)
10141020 Log .debug (self .info_string (), '<-> [' , self .socket .version (), 'handshake complete ]' )
@@ -1056,14 +1062,13 @@ def send(self, byte_data):
10561062 return 0
10571063
10581064 def handle_error (self ):
1059- error_type , value , _traceback = sys .exc_info ()
1060- del _traceback # used to be required in python 2; may no-longer be needed, but best to be safe
10611065 if self .ssl_connection :
10621066 # OSError 0 ('Error') and SSL errors here are caused by connection handshake failures or timeouts
10631067 # APP_PACKAGE is used when we throw our own SSLError on handshake timeout or socket misconfiguration
10641068 ssl_errors = ['SSLV3_ALERT_BAD_CERTIFICATE' , 'PEER_DID_NOT_RETURN_A_CERTIFICATE' , 'WRONG_VERSION_NUMBER' ,
10651069 'CERTIFICATE_VERIFY_FAILED' , 'TLSV1_ALERT_PROTOCOL_VERSION' , 'TLSV1_ALERT_UNKNOWN_CA' ,
10661070 'UNSUPPORTED_PROTOCOL' , APP_PACKAGE ]
1071+ error_type , value = Log .get_last_error ()
10671072 if error_type == OSError and value .errno == 0 or issubclass (error_type , ssl .SSLError ) and \
10681073 any (i in value .args [1 ] for i in ssl_errors ):
10691074 Log .error ('Caught connection error in' , self .info_string (), ':' , error_type , 'with message:' , value )
@@ -1078,7 +1083,7 @@ def handle_error(self):
10781083 'self-signed certificates, but these may still need an exception in your client' )
10791084 Log .error ('If you encounter this error repeatedly, please check that you have correctly configured' ,
10801085 'python root certificates; see: https://github.com/simonrob/email-oauth2-proxy/issues/14' )
1081- self .handle_close ()
1086+ self .close ()
10821087 else :
10831088 super ().handle_error ()
10841089 else :
@@ -1185,7 +1190,9 @@ def log_info(self, message, message_type='info'):
11851190 Log .info (self .info_string (), 'Caught asyncore info message (client) -' , message_type , ':' , message )
11861191
11871192 def handle_close (self ):
1188- Log .debug (self .info_string (), '--> [ Client disconnected ]' )
1193+ error_type , value = Log .get_last_error ()
1194+ if error_type and value :
1195+ Log .info (self .info_string (), 'Caught connection error (client) -' , error_type .__name__ , ':' , value )
11891196 self .close ()
11901197
11911198 def close (self ):
@@ -1196,6 +1203,7 @@ def close(self):
11961203 self .server_connection = None
11971204 self .proxy_parent .remove_client (self )
11981205 with contextlib .suppress (OSError ):
1206+ Log .debug (self .info_string (), '<-- [ Server disconnected ]' )
11991207 super ().close ()
12001208
12011209
@@ -1586,16 +1594,15 @@ def send(self, byte_data, censor_log=False):
15861594 return super ().send (byte_data )
15871595
15881596 def handle_error (self ):
1589- error_type , value , _traceback = sys .exc_info ()
1590- del _traceback # used to be required in python 2; may no-longer be needed, but best to be safe
1597+ error_type , value = Log .get_last_error ()
15911598 if error_type == TimeoutError and value .errno == errno .ETIMEDOUT or \
15921599 issubclass (error_type , ConnectionError ) and value .errno in [errno .ECONNRESET , errno .ECONNREFUSED ] or \
15931600 error_type == OSError and value .errno in [0 , errno .ENETDOWN , errno .EHOSTUNREACH ]:
15941601 # TimeoutError 60 = 'Operation timed out'; ConnectionError 54 = 'Connection reset by peer', 61 = 'Connection
15951602 # refused; OSError 0 = 'Error' (typically network failure), 50 = 'Network is down', 65 = 'No route to host'
15961603 Log .info (self .info_string (), 'Caught network error (server) - is there a network connection?' ,
15971604 'Error type' , error_type , 'with message:' , value )
1598- self .handle_close ()
1605+ self .close ()
15991606 else :
16001607 super ().handle_error ()
16011608
@@ -1605,7 +1612,13 @@ def log_info(self, message, message_type='info'):
16051612 Log .info (self .info_string (), 'Caught asyncore info message (server) -' , message_type , ':' , message )
16061613
16071614 def handle_close (self ):
1608- Log .debug (self .info_string (), '<-- [ Server disconnected ]' )
1615+ error_type , value = Log .get_last_error ()
1616+ if error_type and value :
1617+ message = 'Caught connection error (server)'
1618+ if error_type == OSError and value .errno in [errno .ENOTCONN , 10057 ]:
1619+ # OSError 57 or 10057 = 'Socket is not connected'
1620+ message = '%s [ Client attempted to send command without waiting for server greeting ]' % message
1621+ Log .info (self .info_string (), message , '-' , error_type .__name__ , ':' , value )
16091622 self .close ()
16101623
16111624 def close (self ):
@@ -1615,6 +1628,7 @@ def close(self):
16151628 self .client_connection .close ()
16161629 self .client_connection = None
16171630 with contextlib .suppress (OSError ):
1631+ Log .debug (self .info_string (), '--> [ Client disconnected ]' )
16181632 super ().close ()
16191633
16201634
@@ -1880,7 +1894,7 @@ def handle_accepted(self, connection, address):
18801894 except Exception :
18811895 connection .close ()
18821896 if new_server_connection :
1883- new_server_connection .handle_close ()
1897+ new_server_connection .close ()
18841898 raise
18851899 else :
18861900 error_text = '%s rejecting new connection above MAX_CONNECTIONS limit of %d' % (
@@ -1897,7 +1911,7 @@ def run_server(client, socket_map):
18971911 if not EXITING :
18981912 # OSError 9 = 'Bad file descriptor', thrown when closing connections after network interruption
18991913 if isinstance (e , OSError ) and e .errno == errno .EBADF :
1900- Log .debug (client .info_string (), '[ Connection closed ]' )
1914+ Log .debug (client .info_string (), '[ Connection failed ]' )
19011915 else :
19021916 Log .info (client .info_string (), 'Caught asyncore exception in thread loop:' , Log .error_string (e ))
19031917
@@ -1964,8 +1978,7 @@ def restart(self):
19641978 self .start ()
19651979
19661980 def handle_error (self ):
1967- error_type , value , _traceback = sys .exc_info ()
1968- del _traceback # used to be required in python 2; may no-longer be needed, but best to be safe
1981+ error_type , value = Log .get_last_error ()
19691982 if error_type == socket .gaierror and value .errno in [- 2 , 8 , 11001 ] or \
19701983 error_type == TimeoutError and value .errno == errno .ETIMEDOUT or \
19711984 issubclass (error_type , ConnectionError ) and value .errno in [errno .ECONNRESET , errno .ECONNREFUSED ] or \
@@ -1986,6 +1999,9 @@ def log_info(self, message, message_type='info'):
19861999
19872000 def handle_close (self ):
19882001 # if we encounter an unhandled exception in asyncore, handle_close() is called; restart this server
2002+ error_type , value = Log .get_last_error ()
2003+ if error_type and value :
2004+ Log .info (self .info_string (), 'Caught connection error -' , error_type .__name__ , ':' , value )
19892005 Log .info ('Unexpected close of proxy connection - restarting' , self .info_string ())
19902006 try :
19912007 self .restart ()
0 commit comments