Skip to content

Commit 20d493f

Browse files
committed
Always surface any errors that led to closing a connection
See #101 and #151
1 parent 9041123 commit 20d493f

File tree

1 file changed

+30
-14
lines changed

1 file changed

+30
-14
lines changed

emailproxy.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
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

1111
import abc
1212
import 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

278284
class 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

Comments
 (0)