Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 109 additions & 60 deletions cheroot/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,87 +295,136 @@ def _remove_invalid_sockets(self):
with _cm.suppress(OSError):
conn.close()

def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
def _setup_conn_addr(self, conn, s, addr):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use full words for variable names (things like i/j/k/s aren't really descriptive, nor are the shortened words). It'd also be a good idea to settle for all the args being keyword-only or positional-only (seems like in this case positional args of different types make up bad DX).

"""Configure remote address and port for the connection."""
# Optional values
# Until we do DNS lookups, omit REMOTE_HOST
if addr is None:
# Fallback for sockets that don't return an address on accept
# figure out if AF_INET or AF_INET6.
if len(s.getsockname()) == 2:
# AF_INET
addr = ('0.0.0.0', 0)
else:
# AF_INET6
addr = ('::', 0)
conn.remote_addr = addr[0]
conn.remote_port = addr[1]

def _handle_socket_error(self, ex, s=None):
"""Handle OSErrors and determine if they should be ignored."""
if self.server.stats['Enabled']:
self.server.stats['Socket Errors'] += 1

if s:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps, the if-block would be nicer inside the CM?

with _cm.suppress(OSError):
s.close()

err_code = ex.args[0] if ex.args else None
ignored_groups = (
errors.socket_error_eintr,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you expand all the iterables like this

Suggested change
errors.socket_error_eintr,
*errors.socket_error_eintr,

then you won't need that double-loop generation expression in the if-block.

# I *think* this is right. EINTR should occur when a signal
# is received during the accept() call; all docs say retry
# the call, and I *think* I'm reading it right that Python
# will then go ahead and poll for and handle the signal
# elsewhere. See
# https://github.com/cherrypy/cherrypy/issues/707.
errors.socket_errors_nonblocking,
# Just try again. See
# https://github.com/cherrypy/cherrypy/issues/479.
errors.socket_errors_to_ignore,
# Our socket was closed.
# See https://github.com/cherrypy/cherrypy/issues/686.
)
if any(err_code in group for group in ignored_groups):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, there's itertools.chain() for unwrapping nested things.

return

raise ex

def _wrap_tls_socket(self, s, addr):
"""Handle the TLS wrap and log specific error responses.

on success returns e.g. (SSLSocket, {'SSL_PROTOCOL': 'TLSv1.3', ...}).
on failure returns None, {}
"""
try:
return self.server.ssl_adapter.wrap(s)
except errors.FatalSSLAlert as tls_connection_drop_error:
self.server.error_log(
f'Client {addr!s} lost β€” peer dropped the TLS '
'connection suddenly, during handshake: '
f'{tls_connection_drop_error!s}',
)
except errors.NoSSLError as http_over_https_err:
self.server.error_log(
f'Client {addr!s} attempted to speak plain HTTP into '
'a TCP connection configured for TLS-only traffic β€” '
'trying to send back a plain HTTP error response: '
f'{http_over_https_err!s}',
)
self._send_bad_request_plain_http_error(s)

# If we hit either exception, close the socket and signal failure
with _cm.suppress(OSError):
s.close()
return None, {}

def _create_conn(self, s, addr, ssl_env):
"""Build and configure the Connection object."""
# 1. Determine the makefile type (SSL vs Plain)
mf = MakeFile
if self.server.ssl_adapter is not None:
mf = self.server.ssl_adapter.makefile

# 2. Re-apply timeout specifically for the new SSLSocket object
if hasattr(s, 'settimeout'):
s.settimeout(self.server.timeout)

# 3. Create the actual connection object
conn = self.server.ConnectionClass(self.server, s, mf)
conn.ssl_env = ssl_env

# 4. Configure the remote address/port if it's not a Unix socket
if not isinstance(self.server.bind_addr, (str, bytes)):
self._setup_conn_addr(conn, s, addr)

return conn

def _from_server_socket(self, server_socket):
"""Coordinate socket acceptance and connection initialization."""
try:
s, addr = server_socket.accept()
if self.server.stats['Enabled']:
self.server.stats['Accepts'] += 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid making formatting changes in functional PRs.

prevent_socket_inheritance(s)
if hasattr(s, 'settimeout'):
s.settimeout(self.server.timeout)

mf = MakeFile
ssl_env = {}
# if ssl cert and key are set, we try to be a secure HTTP server
if self.server.ssl_adapter is not None:
# FIXME: WPS505 -- too many nested blocks
try: # noqa: WPS505
s, ssl_env = self.server.ssl_adapter.wrap(s)
except errors.FatalSSLAlert as tls_connection_drop_error:
self.server.error_log(
f'Client {addr!s} lost β€” peer dropped the TLS '
'connection suddenly, during handshake: '
f'{tls_connection_drop_error!s}',
)
return None
except errors.NoSSLError as http_over_https_err:
self.server.error_log(
f'Client {addr!s} attempted to speak plain HTTP into '
'a TCP connection configured for TLS-only traffic β€” '
'trying to send back a plain HTTP error response: '
f'{http_over_https_err!s}',
)
self._send_bad_request_plain_http_error(s)
# try to become a secure server
s, ssl_env = self._wrap_tls_socket(s, addr)
if s is None:
return None
mf = self.server.ssl_adapter.makefile
# Re-apply our timeout since we may have a new socket object

# Re-apply timeout to the new SSLSocket object
if hasattr(s, 'settimeout'):
s.settimeout(self.server.timeout)

conn = self.server.ConnectionClass(self.server, s, mf)

if not isinstance(self.server.bind_addr, (str, bytes)):
# optional values
# Until we do DNS lookups, omit REMOTE_HOST
if addr is None: # sometimes this can happen
# figure out if AF_INET or AF_INET6.
if len(s.getsockname()) == 2:
# AF_INET
addr = ('0.0.0.0', 0)
else:
# AF_INET6
addr = ('::', 0)
conn.remote_addr = addr[0]
conn.remote_port = addr[1]

conn.ssl_env = ssl_env
return conn
return self._create_conn(s, addr, ssl_env)

except socket.timeout:
# The only reason for the timeout in start() is so we can
# notice keyboard interrupts on Win32, which don't interrupt
# accept() by default
return None
except OSError as ex:
if self.server.stats['Enabled']:
self.server.stats['Socket Errors'] += 1
if ex.args[0] in errors.socket_error_eintr:
# I *think* this is right. EINTR should occur when a signal
# is received during the accept() call; all docs say retry
# the call, and I *think* I'm reading it right that Python
# will then go ahead and poll for and handle the signal
# elsewhere. See
# https://github.com/cherrypy/cherrypy/issues/707.
return None
if ex.args[0] in errors.socket_errors_nonblocking:
# Just try again. See
# https://github.com/cherrypy/cherrypy/issues/479.
return None
if ex.args[0] in errors.socket_errors_to_ignore:
# Our socket was closed.
# See https://github.com/cherrypy/cherrypy/issues/686.
return None
raise
# if socket.accept() fails s may not be defined

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So why not initialize it as None earlier, then?

# or if the handshake fails it may exist but
# will need to be closed.
# pass it over to error handler if it exists
return self._handle_socket_error(ex, locals().get('s'))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dynamic locals lookup seems weird here β€” it doesn't make sense to me.


def close(self):
"""Close all monitored connections."""
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog-fragments.d/819.contrib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Refactored ``_from_server_socket()`` in :py:class:`~cheroot.connections.ConnectionManager`.
This function had become highly nested and complex, triggering
linter warnings (C901, WPS505). Decomposed the logic into

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better to spell out what the violations are instead of using the trampoline codes β€” people don't tend to memorize what these numbers are anyway.

several smaller private methods.
-- by :user:`julianz-`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- by :user:`julianz-`
-- by :user:`julianz-`

Loading