<reponame>ajyoung/cheroot<gh_stars>0
"""Utilities to manage open connections."""

from __future__ import absolute_import, division, print_function
__metaclass__ = type

import io
import os
import socket
import time

from . import errors
from ._compat import selectors
from .makefile import MakeFile

import six

try:
    import fcntl
except ImportError:
    try:
        from ctypes import windll, WinError
        import ctypes.wintypes
        _SetHandleInformation = windll.kernel32.SetHandleInformation
        _SetHandleInformation.argtypes = [
            ctypes.wintypes.HANDLE,
            ctypes.wintypes.DWORD,
            ctypes.wintypes.DWORD,
        ]
        _SetHandleInformation.restype = ctypes.wintypes.BOOL
    except ImportError:
        def prevent_socket_inheritance(sock):
            """Stub inheritance prevention.

            Dummy function, since neither fcntl nor ctypes are available.
            """
            pass
    else:
        def prevent_socket_inheritance(sock):
            """Mark the given socket fd as non-inheritable (Windows)."""
            if not _SetHandleInformation(sock.fileno(), 1, 0):
                raise WinError()
else:
    def prevent_socket_inheritance(sock):
        """Mark the given socket fd as non-inheritable (POSIX)."""
        fd = sock.fileno()
        old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
        fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)


class ConnectionManager:
    """Class which manages HTTPConnection objects.

    This is for connections which are being kept-alive for follow-up requests.
    """

    def __init__(self, server):
        """Initialize ConnectionManager object.

        Args:
            server (cheroot.server.HTTPServer): web server object
                that uses this ConnectionManager instance.
        """
        self.server = server
        self.connections = []
        self._selector = selectors.DefaultSelector()

    def put(self, conn):
        """Put idle connection into the ConnectionManager to be managed.

        Args:
            conn (cheroot.server.HTTPConnection): HTTP connection
                to be managed.
        """
        conn.last_used = time.time()
        conn.ready_with_data = conn.rfile.has_data()
        self.connections.append(conn)

    def expire(self):
        """Expire least recently used connections.

        This happens if there are either too many open connections, or if the
        connections have been timed out.

        This should be called periodically.
        """
        if not self.connections:
            return

        # Look at the first connection - if it can be closed, then do
        # that, and wait for get_conn to return it.
        conn = self.connections[0]
        if conn.closeable:
            return

        # Connection too old?
        if (conn.last_used + self.server.timeout) < time.time():
            conn.closeable = True
            return

    def get_conn(self, server_socket):
        """Return a HTTPConnection object which is ready to be handled.

        A connection returned by this method should be ready for a worker
        to handle it. If there are no connections ready, None will be
        returned.

        Any connection returned by this method will need to be `put`
        back if it should be examined again for another request.

        Args:
            server_socket (socket.socket): Socket to listen to for new
            connections.
        Returns:
            cheroot.server.HTTPConnection instance, or None.

        """
        # Grab file descriptors from sockets, but stop if we find a
        # connection which is already marked as ready.
        socket_dict = {}
        for conn in self.connections:
            if conn.closeable or conn.ready_with_data:
                break
            socket_dict[conn.socket.fileno()] = conn
        else:
            # No ready connection.
            conn = None

        # We have a connection ready for use.
        if conn:
            self.connections.remove(conn)
            return conn

        # Will require a select call.
        ss_fileno = server_socket.fileno()
        socket_dict[ss_fileno] = server_socket
        try:
            for fno in socket_dict:
                self._selector.register(fno, selectors.EVENT_READ)
            # The timeout value impacts performance and should be carefully
            # chosen. Ref:
            # github.com/cherrypy/cheroot/issues/305#issuecomment-663985165
            rlist = [
                key.fd for key, _event
                in self._selector.select(timeout=0.01)
            ]
        except OSError:
            # Mark any connection which no longer appears valid.
            for fno, conn in list(socket_dict.items()):
                # If the server socket is invalid, we'll just ignore it and
                # wait to be shutdown.
                if fno == ss_fileno:
                    continue
                try:
                    os.fstat(fno)
                except OSError:
                    # Socket is invalid, close the connection, insert at
                    # the front.
                    self.connections.remove(conn)
                    self.connections.insert(0, conn)
                    conn.closeable = True

            # Wait for the next tick to occur.
            return None
        finally:
            for fno in socket_dict:
                self._selector.unregister(fno)

        try:
            # See if we have a new connection coming in.
            rlist.remove(ss_fileno)
        except ValueError:
            # If we didn't get any readable sockets, wait for the next tick
            if not rlist:
                return None

            # No new connection, but reuse an existing socket.
            conn = socket_dict[rlist.pop()]
        else:
            # If we have a new connection, reuse the server socket
            conn = server_socket

        # All remaining connections in rlist should be marked as ready.
        for fno in rlist:
            socket_dict[fno].ready_with_data = True

        # New connection.
        if conn is server_socket:
            return self._from_server_socket(server_socket)

        self.connections.remove(conn)
        return conn

    def _from_server_socket(self, server_socket):
        try:
            s, addr = server_socket.accept()
            if self.server.stats['Enabled']:
                self.server.stats['Accepts'] += 1
            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:
                try:
                    s, ssl_env = self.server.ssl_adapter.wrap(s)
                except errors.NoSSLError:
                    msg = (
                        'The client sent a plain HTTP request, but '
                        'this server only speaks HTTPS on this port.'
                    )
                    buf = [
                        '%s 400 Bad Request\r\n' % self.server.protocol,
                        'Content-Length: %s\r\n' % len(msg),
                        'Content-Type: text/plain\r\n\r\n',
                        msg,
                    ]

                    sock_to_make = s if not six.PY2 else s._sock
                    wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE)
                    try:
                        wfile.write(''.join(buf).encode('ISO-8859-1'))
                    except socket.error as ex:
                        if ex.args[0] not in errors.socket_errors_to_ignore:
                            raise
                    return
                if not s:
                    return
                mf = self.server.ssl_adapter.makefile
                # Re-apply our timeout since we may have a new socket 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,
                    (six.text_type, six.binary_type),
            ):
                # 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

        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
        except socket.error 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
            if ex.args[0] in errors.socket_errors_nonblocking:
                # Just try again. See
                # https://github.com/cherrypy/cherrypy/issues/479.
                return
            if ex.args[0] in errors.socket_errors_to_ignore:
                # Our socket was closed.
                # See https://github.com/cherrypy/cherrypy/issues/686.
                return
            raise

    def close(self):
        """Close all monitored connections."""
        for conn in self.connections[:]:
            conn.close()
        self.connections = []

    @property
    def can_add_keepalive_connection(self):
        """Flag whether it is allowed to add a new keep-alive connection."""
        ka_limit = self.server.keep_alive_conn_limit
        return ka_limit is None or len(self.connections) < ka_limit

"""Utilities to manage open connections."""

from __future__ import absolute_import, division, print_function
__metaclass__ = type

import io
import os
import socket
import time

from . import errors
from ._compat import selectors
from .makefile import MakeFile

import six

try:
    import fcntl
except ImportError:
    try:
        from ctypes import windll, WinError
        import ctypes.wintypes
        _SetHandleInformation = windll.kernel32.SetHandleInformation
        _SetHandleInformation.argtypes = [
            ctypes.wintypes.HANDLE,
            ctypes.wintypes.DWORD,
            ctypes.wintypes.DWORD,
        ]
        _SetHandleInformation.restype = ctypes.wintypes.BOOL
    except ImportError:
        def prevent_socket_inheritance(sock):
            """Stub inheritance prevention.

            Dummy function, since neither fcntl nor ctypes are available.
            """
            pass
    else:
        def prevent_socket_inheritance(sock):
            """Mark the given socket fd as non-inheritable (Windows)."""
            if not _SetHandleInformation(sock.fileno(), 1, 0):
                raise WinError()
else:
    def prevent_socket_inheritance(sock):
        """Mark the given socket fd as non-inheritable (POSIX)."""
        fd = sock.fileno()
        old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
        fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)


class ConnectionManager:
    """Class which manages HTTPConnection objects.

    This is for connections which are being kept-alive for follow-up requests.
    """

    def __init__(self, server):
        """Initialize ConnectionManager object.

        Args:
            server (cheroot.server.HTTPServer): web server object
                that uses this ConnectionManager instance.
        """
        self.server = server
        self.connections = []
        self._selector = selectors.DefaultSelector()

    def put(self, conn):
        """Put idle connection into the ConnectionManager to be managed.

        Args:
            conn (cheroot.server.HTTPConnection): HTTP connection
                to be managed.
        """
        conn.last_used = time.time()
        conn.ready_with_data = conn.rfile.has_data()
        self.connections.append(conn)

    def expire(self):
        """Expire least recently used connections.

        This happens if there are either too many open connections, or if the
        connections have been timed out.

        This should be called periodically.
        """
        if not self.connections:
            return

        # Look at the first connection - if it can be closed, then do
        # that, and wait for get_conn to return it.
        conn = self.connections[0]
        if conn.closeable:
            return

        # Connection too old?
        if (conn.last_used + self.server.timeout) < time.time():
            conn.closeable = True
            return

    def get_conn(self, server_socket):
        """Return a HTTPConnection object which is ready to be handled.

        A connection returned by this method should be ready for a worker
        to handle it. If there are no connections ready, None will be
        returned.

        Any connection returned by this method will need to be `put`
        back if it should be examined again for another request.

        Args:
            server_socket (socket.socket): Socket to listen to for new
            connections.
        Returns:
            cheroot.server.HTTPConnection instance, or None.

        """
        # Grab file descriptors from sockets, but stop if we find a
        # connection which is already marked as ready.
        socket_dict = {}
        for conn in self.connections:
            if conn.closeable or conn.ready_with_data:
                break
            socket_dict[conn.socket.fileno()] = conn
        else:
            # No ready connection.
            conn = None

        # We have a connection ready for use.
        if conn:
            self.connections.remove(conn)
            return conn

        # Will require a select call.
        ss_fileno = server_socket.fileno()
        socket_dict[ss_fileno] = server_socket
        try:
            for fno in socket_dict:
                self._selector.register(fno, selectors.EVENT_READ)
            # The timeout value impacts performance and should be carefully
            # chosen. Ref:
            # github.com/cherrypy/cheroot/issues/305#issuecomment-663985165
            rlist = [
                key.fd for key, _event
                in self._selector.select(timeout=0.01)
            ]
        except OSError:
            # Mark any connection which no longer appears valid.
            for fno, conn in list(socket_dict.items()):
                # If the server socket is invalid, we'll just ignore it and
                # wait to be shutdown.
                if fno == ss_fileno:
                    continue
                try:
                    os.fstat(fno)
                except OSError:
                    # Socket is invalid, close the connection, insert at
                    # the front.
                    self.connections.remove(conn)
                    self.connections.insert(0, conn)
                    conn.closeable = True

            # Wait for the next tick to occur.
            return None
        finally:
            for fno in socket_dict:
                self._selector.unregister(fno)

        try:
            # See if we have a new connection coming in.
            rlist.remove(ss_fileno)
        except ValueError:
            # If we didn't get any readable sockets, wait for the next tick
            if not rlist:
                return None

            # No new connection, but reuse an existing socket.
            conn = socket_dict[rlist.pop()]
        else:
            # If we have a new connection, reuse the server socket
            conn = server_socket

        # All remaining connections in rlist should be marked as ready.
        for fno in rlist:
            socket_dict[fno].ready_with_data = True

        # New connection.
        if conn is server_socket:
            return self._from_server_socket(server_socket)

        self.connections.remove(conn)
        return conn

    def _from_server_socket(self, server_socket):
        try:
            s, addr = server_socket.accept()
            if self.server.stats['Enabled']:
                self.server.stats['Accepts'] += 1
            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:
                try:
                    s, ssl_env = self.server.ssl_adapter.wrap(s)
                except errors.NoSSLError:
                    msg = (
                        'The client sent a plain HTTP request, but '
                        'this server only speaks HTTPS on this port.'
                    )
                    buf = [
                        '%s 400 Bad Request\r\n' % self.server.protocol,
                        'Content-Length: %s\r\n' % len(msg),
                        'Content-Type: text/plain\r\n\r\n',
                        msg,
                    ]

                    sock_to_make = s if not six.PY2 else s._sock
                    wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE)
                    try:
                        wfile.write(''.join(buf).encode('ISO-8859-1'))
                    except socket.error as ex:
                        if ex.args[0] not in errors.socket_errors_to_ignore:
                            raise
                    return
                if not s:
                    return
                mf = self.server.ssl_adapter.makefile
                # Re-apply our timeout since we may have a new socket 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,
                    (six.text_type, six.binary_type),
            ):
                # 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

        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
        except socket.error 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
            if ex.args[0] in errors.socket_errors_nonblocking:
                # Just try again. See
                # https://github.com/cherrypy/cherrypy/issues/479.
                return
            if ex.args[0] in errors.socket_errors_to_ignore:
                # Our socket was closed.
                # See https://github.com/cherrypy/cherrypy/issues/686.
                return
            raise

    def close(self):
        """Close all monitored connections."""
        for conn in self.connections[:]:
            conn.close()
        self.connections = []

    @property
    def can_add_keepalive_connection(self):
        """Flag whether it is allowed to add a new keep-alive connection."""
        ka_limit = self.server.keep_alive_conn_limit
        return ka_limit is None or len(self.connections) < ka_limit
Utilities to manage open connections. Stub inheritance prevention.              Dummy function, since neither fcntl nor ctypes are available. Mark the given socket fd as non-inheritable (Windows). Mark the given socket fd as non-inheritable (POSIX). Class which manages HTTPConnection objects.      This is for connections which are being kept-alive for follow-up requests. Initialize ConnectionManager object.          Args:             server (cheroot.server.HTTPServer): web server object                 that uses this ConnectionManager instance. Put idle connection into the ConnectionManager to be managed.          Args:             conn (cheroot.server.HTTPConnection): HTTP connection                 to be managed. Expire least recently used connections.          This happens if there are either too many open connections, or if the         connections have been timed out.          This should be called periodically. # Look at the first connection - if it can be closed, then do # that, and wait for get_conn to return it. # Connection too old? Return a HTTPConnection object which is ready to be handled.          A connection returned by this method should be ready for a worker         to handle it. If there are no connections ready, None will be         returned.          Any connection returned by this method will need to be `put`         back if it should be examined again for another request.          Args:             server_socket (socket.socket): Socket to listen to for new             connections.         Returns:             cheroot.server.HTTPConnection instance, or None. # Grab file descriptors from sockets, but stop if we find a # connection which is already marked as ready. # No ready connection. # We have a connection ready for use. # Will require a select call. # The timeout value impacts performance and should be carefully # chosen. Ref: # github.com/cherrypy/cheroot/issues/305#issuecomment-663985165 # Mark any connection which no longer appears valid. # If the server socket is invalid, we'll just ignore it and # wait to be shutdown. # Socket is invalid, close the connection, insert at # the front. # Wait for the next tick to occur. # See if we have a new connection coming in. # If we didn't get any readable sockets, wait for the next tick # No new connection, but reuse an existing socket. # If we have a new connection, reuse the server socket # All remaining connections in rlist should be marked as ready. # New connection. # if ssl cert and key are set, we try to be a secure HTTP server # Re-apply our timeout since we may have a new socket object # optional values # Until we do DNS lookups, omit REMOTE_HOST # sometimes this can happen # figure out if AF_INET or AF_INET6. # AF_INET # AF_INET6 # The only reason for the timeout in start() is so we can # notice keyboard interrupts on Win32, which don't interrupt # accept() by default # 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. # Just try again. See # https://github.com/cherrypy/cherrypy/issues/479. # Our socket was closed. # See https://github.com/cherrypy/cherrypy/issues/686. Close all monitored connections. Flag whether it is allowed to add a new keep-alive connection.