diff --git a/cheroot/ssl/builtin.py b/cheroot/ssl/builtin.py index 1cec20255d..077bb6ecce 100644 --- a/cheroot/ssl/builtin.py +++ b/cheroot/ssl/builtin.py @@ -276,7 +276,7 @@ def wrap(self, sock): try: s = self.context.wrap_socket( sock, - do_handshake_on_connect=True, + do_handshake_on_connect=False, server_side=True, ) except ( @@ -306,6 +306,97 @@ def wrap(self, sock): return s, self.get_environ(s) + def _handle_plain_http_error(self, wfile, buf): + try: + wfile.write(buf) + except OSError as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + + def _handle_handshake_failure(self, conn): + try: + conn.socket.shutdown(socket.SHUT_RDWR) + except Exception: + # pass + return + + def do_handshake(self, conn): + """Process SSL handshake on connection if needed. + + Args: + conn (:py:class:`~cheroot.server.HTTPConnection`): HTTP connection + """ + ssl_handshake_must_be_done = ( + conn + and getattr(conn, 'socket', None) + and getattr(conn.socket, 'do_handshake', None) + and not getattr(conn.socket, 'cheroot_handshake_done', False) + ) + if ssl_handshake_must_be_done: + conn.socket.cheroot_handshake_done = True + do_shutdown = False + try: + conn.socket.do_handshake() + except ssl.SSLError as generic_tls_error: + do_shutdown = True + + # Try to send HTTP 400 status for plain text queries + peer_speaks_plain_http_over_https = ( + generic_tls_error.errno == ssl.SSL_ERROR_SSL + and _assert_ssl_exc_contains( + generic_tls_error, + 'http request', + ) + ) + if peer_speaks_plain_http_over_https: + msg = ( + 'The client %s:%s sent a plain HTTP request, but ' + 'this server only speaks HTTPS on this port.' + ) + msg = msg % (conn.remote_addr, conn.remote_port) + buf = [ + '%s 400 Bad Request\r\n' % conn.server.protocol, + 'Content-Length: %s\r\n' % len(msg), + 'Content-Type: text/plain\r\n\r\n', + msg, + ] + conn.server.error_log(msg) + + # - writing directly on conn.socket attempt to use + # non-initialized SSL layer and raises exception: + # EOF occurred in violation of protocol (_ssl.c:2427) + # - conn.socket.unwrap fails raising exception: + # [SSL: SHUTDOWN_WHILE_IN_INIT] shutdown while in + # init (_ssl.c:2706) + # - we create a non SSL socket to send plain text reply + conn.socket = socket.socket(fileno=conn.socket.detach()) + wfile = StreamWriter( + conn.socket, + 'wb', + DEFAULT_BUFFER_SIZE, + ) + buf = ''.join(buf).encode('ISO-8859-1') + self._handle_plain_http_error(wfile, buf) + else: + msg = 'SSL handshake for %s:%s failed with SSL error:%s' + msg = msg % ( + conn.remote_addr, + conn.remote_port, + generic_tls_error, + ) + conn.server.error_log(msg) + + except Exception as generic_error: + do_shutdown = True + + msg = 'SSL handshake for %s:%s failed with error:%s' + msg = msg % (conn.remote_addr, conn.remote_port, generic_error) + conn.server.error_log(msg) + + finally: + if do_shutdown: + self._handle_handshake_failure(conn) + def get_environ(self, sock): """Create WSGI environ entries to be merged into each request.""" cipher = sock.cipher() diff --git a/cheroot/ssl/builtin.pyi b/cheroot/ssl/builtin.pyi index ca106aca12..fcf896dad6 100644 --- a/cheroot/ssl/builtin.pyi +++ b/cheroot/ssl/builtin.pyi @@ -20,5 +20,6 @@ class BuiltinSSLAdapter(Adapter): def context(self, context) -> None: ... def bind(self, sock): ... def wrap(self, sock): ... + def do_handshake(self, conn) -> None: ... def get_environ(self, sock): ... def makefile(self, sock, mode: str = ..., bufsize: int = ...): ... diff --git a/cheroot/workers/threadpool.py b/cheroot/workers/threadpool.py index a68da6d214..eead5e3e69 100644 --- a/cheroot/workers/threadpool.py +++ b/cheroot/workers/threadpool.py @@ -276,7 +276,6 @@ def __init__( self._threads = [] self._queue = queue.Queue(maxsize=accepted_queue_size) self._queue_put_timeout = accepted_queue_timeout - self.get = self._queue.get self._pending_shutdowns = collections.deque() def start(self): @@ -294,6 +293,24 @@ def idle(self): # noqa: D401; irrelevant for properties idles = len([t for t in self._threads if t.conn is None]) return max(idles - len(self._pending_shutdowns), 0) + def get(self): + """Get request from queue, and process SSL handshake is needed. + + Return: + conn (:py:class:`~cheroot.server.HTTPConnection`): HTTP connection + ready to be processed + + """ + conn = self._queue.get() + ssl_adapter = self.server.ssl_adapter + check_for_ssl_handshake = ( + ssl_adapter is not None + and getattr(ssl_adapter, 'do_handshake', None) is not None + ) + if check_for_ssl_handshake: + ssl_adapter.do_handshake(conn) + return conn + def put(self, obj): """Put request into queue. diff --git a/cheroot/workers/threadpool.pyi b/cheroot/workers/threadpool.pyi index 02a09b6c90..89c4e7e7a7 100644 --- a/cheroot/workers/threadpool.pyi +++ b/cheroot/workers/threadpool.pyi @@ -24,7 +24,6 @@ class ThreadPool: server: Any min: Any max: Any - get: Any def __init__( self, server, @@ -36,6 +35,7 @@ class ThreadPool: def start(self) -> None: ... @property def idle(self): ... + def get(self) -> Any: ... def put(self, obj) -> None: ... def grow(self, amount) -> None: ... def shrink(self, amount) -> None: ...