Esempio n. 1
0
    def request_headers(self, environ):
        '''Modify request headers via the list of :attr:`headers_middleware`.
The returned headers will be sent to the target uri.'''
        headers = Headers(kind='client')
        for k in environ:
            if k.startswith('HTTP_'):
                head = k[5:].replace('_','-')
                headers[head] = environ[k]
        for head in ENVIRON_HEADERS:
            k = head.replace('-','_').upper()
            v = environ.get(k)
            if v:
                headers[head] = v
        headers.update(self.headers)
        for middleware in self.headers_middleware:
            middleware(environ, headers)
        return headers
Esempio n. 2
0
class HttpServerResponse(ProtocolConsumer):
    '''Server side WSGI :class:`.ProtocolConsumer`.

    .. attribute:: wsgi_callable

        The wsgi callable handling requests.
    '''
    _status = None
    _headers_sent = None
    _stream = None
    _buffer = None
    SERVER_SOFTWARE = pulsar.SERVER_SOFTWARE
    ONE_TIME_EVENTS = ProtocolConsumer.ONE_TIME_EVENTS + ('on_headers', )

    def __init__(self, wsgi_callable, cfg, server_software=None, loop=None):
        super(HttpServerResponse, self).__init__(loop=loop)
        self.wsgi_callable = wsgi_callable
        self.cfg = cfg
        self.parser = http_parser(kind=0)
        self.headers = Headers()
        self.keep_alive = False
        self.SERVER_SOFTWARE = server_software or self.SERVER_SOFTWARE

    @property
    def headers_sent(self):
        '''Available once the headers have been sent to the client.

        These are the bytes representing the first response line and
        the headers
        '''
        return self._headers_sent

    def data_received(self, data):
        '''Implements :meth:`~.ProtocolConsumer.data_received` method.

        Once we have a full HTTP message, build the wsgi ``environ`` and
        delegate the response to the :func:`wsgi_callable` function.
        '''
        parser = self.parser
        processed = parser.execute(data, len(data))
        if not self._stream and parser.is_headers_complete():
            headers = Headers(parser.get_headers(), kind='client')
            self._stream = StreamReader(headers, parser, self.transport)
            self._response(self.wsgi_environ())
        #
        if parser.is_message_complete():
            #
            # Stream has the whole body
            if not self._stream.on_message_complete.done():
                self._stream.on_message_complete.set_result(None)

            if processed < len(data):
                if not self._buffer:
                    self._buffer = data[processed:]
                    self.bind_event('post_request', self._new_request)
                else:
                    self._buffer += data[processed:]
        #
        elif processed < len(data):
            # This is a parsing error, the client must have sent
            # bogus data
            raise ProtocolError

    @property
    def status(self):
        return self._status

    @property
    def upgrade(self):
        return self.headers.get('upgrade')

    @property
    def chunked(self):
        return self.headers.get('Transfer-Encoding') == 'chunked'

    @property
    def content_length(self):
        c = self.headers.get('Content-Length')
        if c:
            return int(c)

    @property
    def version(self):
        return self.parser.get_version()

    def start_response(self, status, response_headers, exc_info=None):
        '''WSGI compliant ``start_response`` callable, see pep3333_.

        The application may call start_response more than once, if and only
        if the ``exc_info`` argument is provided.
        More precisely, it is a fatal error to call ``start_response`` without
        the ``exc_info`` argument if start_response has already been called
        within the current invocation of the application.

        :parameter status: an HTTP ``status`` string like ``200 OK`` or
            ``404 Not Found``.
        :parameter response_headers: a list of ``(header_name, header_value)``
            tuples. It must be a Python list. Each header_name must be a valid
            HTTP header field-name (as defined by RFC 2616_, Section 4.2),
            without a trailing colon or other punctuation.
        :parameter exc_info: optional python ``sys.exc_info()`` tuple.
            This argument should be supplied by the application only if
            ``start_response`` is being called by an error handler.
        :return: The :meth:`write` method.

        ``HOP_HEADERS`` are not considered but no error is raised.

        .. _pep3333: http://www.python.org/dev/peps/pep-3333/
        .. _2616: http://www.faqs.org/rfcs/rfc2616.html
        '''
        if exc_info:
            try:
                if self._headers_sent:
                    # if exc_info is provided, and the HTTP headers have
                    # already been sent, start_response must raise an error,
                    # and should re-raise using the exc_info tuple
                    reraise(*exc_info)
            finally:
                # Avoid circular reference
                exc_info = None
        elif self._status:
            # Headers already set. Raise error
            raise HttpException("Response headers already set!")
        self._status = status
        if type(response_headers) is not list:
            raise TypeError("Headers must be a list of name/value tuples")
        for header, value in response_headers:
            if header.lower() in HOP_HEADERS:
                # These features are the exclusive province of this class,
                # this should be considered a fatal error for an application
                # to attempt sending them, but we don't raise an error,
                # just log a warning
                self.logger.warning('Application passing hop header "%s"',
                                    header)
                continue
            self.headers.add_header(header, value)
        return self.write

    def write(self, data, force=False):
        '''The write function returned by the :meth:`start_response` method.

        Required by the WSGI specification.

        :param data: bytes to write
        :param force: Optional flag used internally
        :return: a :class:`~asyncio.Future` or the number of bytes written
        '''
        write = super(HttpServerResponse, self).write
        chunks = []
        if not self._headers_sent:
            tosend = self.get_headers()
            self._headers_sent = tosend.flat(self.version, self.status)
            self.fire_event('on_headers')
            chunks.append(self._headers_sent)
        if data:
            if self.chunked:
                while len(data) >= MAX_CHUNK_SIZE:
                    chunk, data = data[:MAX_CHUNK_SIZE], data[MAX_CHUNK_SIZE:]
                    chunks.append(chunk_encoding(chunk))
                if data:
                    chunks.append(chunk_encoding(data))
            else:
                chunks.append(data)
        elif force and self.chunked:
            chunks.append(chunk_encoding(data))
        if chunks:
            return write(b''.join(chunks))

    ########################################################################
    #    INTERNALS
    @task
    def _response(self, environ):
        exc_info = None
        response = None
        done = False
        while not done:
            done = True
            try:
                if exc_info is None:
                    if 'SERVER_NAME' not in environ:
                        raise HttpException(status=400)
                    response = self.wsgi_callable(environ, self.start_response)
                else:
                    response = handle_wsgi_error(environ, exc_info)
                #
                if isfuture(response):
                    response = yield From(response)
                #
                if exc_info:
                    self.start_response(response.status,
                                        response.get_headers(), exc_info)
                #
                # Do the actual writing
                loop = self._loop
                start = loop.time()
                for chunk in response:
                    if isfuture(chunk):
                        chunk = yield From(chunk)
                        start = loop.time()
                    result = self.write(chunk)
                    if isfuture(result):
                        yield From(result)
                        start = loop.time()
                    else:
                        time_in_loop = loop.time() - start
                        if time_in_loop > MAX_TIME_IN_LOOP:
                            self.logger.debug(
                                'Released the event loop after %.3f seconds',
                                time_in_loop)
                            yield From(None)
                            start = loop.time()
                #
                # make sure we write headers and last chunk if needed
                self.write(b'', True)

            except IOError:  # client disconnected, end this connection
                self.finished()
            except Exception:
                if wsgi_request(environ).cache.handle_wsgi_error:
                    self.keep_alive = False
                    self.connection.close()
                    self.finished()
                else:
                    done = False
                    exc_info = sys.exc_info()
            else:
                if not self.keep_alive:
                    self.connection.close()
                self.finished()
            finally:
                if hasattr(response, 'close'):
                    try:
                        response.close()
                    except Exception:
                        self.logger.exception(
                            'Error while closing wsgi iterator')

    def is_chunked(self):
        '''Check if the response uses chunked transfer encoding.

        Only use chunked responses when the client is speaking HTTP/1.1
        or newer and there was no Content-Length header set.
        '''
        if (self.version <= (1, 0)
                or self._status == '200 Connection established'
                or has_empty_content(int(self.status[:3]))):
            return False
        elif self.headers.get('Transfer-Encoding') == 'chunked':
            return True
        else:
            return self.content_length is None

    def get_headers(self):
        '''Get the headers to send to the client.
        '''
        if not self._status:
            # we are sending headers but the start_response was not called
            raise HttpException('Headers not set.')
        headers = self.headers
        # Set chunked header if needed
        if self.is_chunked():
            headers['Transfer-Encoding'] = 'chunked'
            headers.pop('content-length', None)
        else:
            headers.pop('Transfer-Encoding', None)
        if self.keep_alive:
            self.keep_alive = keep_alive_with_status(self._status, headers)
        if not self.keep_alive:
            headers['connection'] = 'close'
        return headers

    def wsgi_environ(self):
        # return a the WSGI environ dictionary
        transport = self.transport
        https = True if is_tls(transport.get_extra_info('socket')) else False
        multiprocess = (self.cfg.concurrency == 'process')
        environ = wsgi_environ(self._stream,
                               transport.get_extra_info('sockname'),
                               self.address,
                               self.headers,
                               self.SERVER_SOFTWARE,
                               https=https,
                               extra={
                                   'pulsar.connection': self.connection,
                                   'pulsar.cfg': self.cfg,
                                   'wsgi.multiprocess': multiprocess
                               })
        self.keep_alive = keep_alive(self.headers, self.parser.get_version())
        self.headers.update([('Server', self.SERVER_SOFTWARE),
                             ('Date', format_date_time(time.time()))])
        return environ

    def _new_request(self, _, exc=None):
        connection = self._connection
        connection.data_received(self._buffer)
Esempio n. 3
0
class HttpServerResponse(ProtocolConsumer):
    '''Server side WSGI :class:`.ProtocolConsumer`.

    .. attribute:: wsgi_callable

        The wsgi callable handling requests.
    '''
    _status = None
    _headers_sent = None
    _stream = None
    _buffer = None
    SERVER_SOFTWARE = pulsar.SERVER_SOFTWARE
    ONE_TIME_EVENTS = ProtocolConsumer.ONE_TIME_EVENTS + ('on_headers',)

    def __init__(self, wsgi_callable, cfg, server_software=None):
        super(HttpServerResponse, self).__init__()
        self.wsgi_callable = wsgi_callable
        self.cfg = cfg
        self.parser = http_parser(kind=0)
        self.headers = Headers()
        self.keep_alive = False
        self.SERVER_SOFTWARE = server_software or self.SERVER_SOFTWARE

    def data_received(self, data):
        '''Implements :meth:`~.ProtocolConsumer.data_received` method.

        Once we have a full HTTP message, build the wsgi ``environ`` and
        delegate the response to the :func:`wsgi_callable` function.
        '''
        parser = self.parser
        processed = parser.execute(data, len(data))
        if not self._stream and parser.is_headers_complete():
            headers = Headers(parser.get_headers(), kind='client')
            self._stream = StreamReader(headers, parser, self.transport)
            self._response(self.wsgi_environ())
        #
        done = parser.is_message_complete()
        if done and not self._stream.on_message_complete.done():
            self._stream.on_message_complete.set_result(None)
        #
        if processed < len(data):
            if not done:
                # This is a parsing error, the client must have sent
                # bogus data
                raise ProtocolError
            else:
                if not self._buffer:
                    self._buffer = data[processed:]
                    self.bind_event('post_request', self._new_request)
                else:
                    self._buffer += data[processed:]

    @property
    def status(self):
        return self._status

    @property
    def upgrade(self):
        return self.headers.get('upgrade')

    @property
    def chunked(self):
        return self.headers.get('Transfer-Encoding') == 'chunked'

    @property
    def content_length(self):
        c = self.headers.get('Content-Length')
        if c:
            return int(c)

    @property
    def version(self):
        return self.parser.get_version()

    def start_response(self, status, response_headers, exc_info=None):
        '''WSGI compliant ``start_response`` callable, see pep3333_.

        The application may call start_response more than once, if and only
        if the ``exc_info`` argument is provided.
        More precisely, it is a fatal error to call ``start_response`` without
        the ``exc_info`` argument if start_response has already been called
        within the current invocation of the application.

        :parameter status: an HTTP ``status`` string like ``200 OK`` or
            ``404 Not Found``.
        :parameter response_headers: a list of ``(header_name, header_value)``
            tuples. It must be a Python list. Each header_name must be a valid
            HTTP header field-name (as defined by RFC 2616_, Section 4.2),
            without a trailing colon or other punctuation.
        :parameter exc_info: optional python ``sys.exc_info()`` tuple.
            This argument should be supplied by the application only if
            ``start_response`` is being called by an error handler.
        :return: The :meth:`write` method.

        ``HOP_HEADERS`` are not considered but no error is raised.

        .. _pep3333: http://www.python.org/dev/peps/pep-3333/
        .. _2616: http://www.faqs.org/rfcs/rfc2616.html
        '''
        if exc_info:
            try:
                if self._headers_sent:
                    # if exc_info is provided, and the HTTP headers have
                    # already been sent, start_response must raise an error,
                    # and should re-raise using the exc_info tuple
                    reraise(*exc_info)
            finally:
                # Avoid circular reference
                exc_info = None
        elif self._status:
            # Headers already set. Raise error
            raise HttpException("Response headers already set!")
        self._status = status
        if type(response_headers) is not list:
            raise TypeError("Headers must be a list of name/value tuples")
        for header, value in response_headers:
            if header.lower() in HOP_HEADERS:
                # These features are the exclusive province of this class,
                # this should be considered a fatal error for an application
                # to attempt sending them, but we don't raise an error,
                # just log a warning
                self.logger.warning('Application passing hop header "%s"',
                                    header)
                continue
            self.headers.add_header(header, value)
        return self.write

    def write(self, data, force=False):
        '''The write function returned by the :meth:`start_response` method.

        Required by the WSGI specification.

        :param data: bytes to write
        :param force: Optional flag used internally.
        '''
        if not self._headers_sent:
            tosend = self.get_headers()
            self._headers_sent = tosend.flat(self.version, self.status)
            self.fire_event('on_headers')
            self.transport.write(self._headers_sent)
        if data:
            if self.chunked:
                chunks = []
                while len(data) >= MAX_CHUNK_SIZE:
                    chunk, data = data[:MAX_CHUNK_SIZE], data[MAX_CHUNK_SIZE:]
                    chunks.append(chunk_encoding(chunk))
                if data:
                    chunks.append(chunk_encoding(data))
                self.transport.write(b''.join(chunks))
            else:
                self.transport.write(data)
        elif force and self.chunked:
            self.transport.write(chunk_encoding(data))

    ########################################################################
    ##    INTERNALS
    @in_loop
    def _response(self, environ):
        exc_info = None
        response = None
        done = False
        while not done:
            done = True
            try:
                if exc_info is None:
                    if 'SERVER_NAME' not in environ:
                        raise HttpException(status=400)
                    response = self.wsgi_callable(environ, self.start_response)
                else:
                    response = handle_wsgi_error(environ, exc_info)
                #
                if isinstance(response, Future):
                    response = yield response
                #
                if exc_info:
                    self.start_response(response.status,
                                        response.get_headers(), exc_info)
                #
                for chunk in response:
                    if isinstance(chunk, Future):
                        chunk = yield chunk
                    self.write(chunk)
                #
                # make sure we write headers
                self.write(b'', True)

            except IOError:     # client disconnected, end this connection
                self.finished()
            except Exception as exc:
                if wsgi_request(environ).cache.handle_wsgi_error:
                    self.keep_alive = False
                    self.connection.close()
                    self.finished()
                else:
                    done = False
                    exc_info = sys.exc_info()
            else:
                if not self.keep_alive:
                    self.connection.close()
                self.finished()
            finally:
                if hasattr(response, 'close'):
                    try:
                        response.close()
                    except Exception:
                        self.logger.exception(
                            'Error while closing wsgi iterator')

    def is_chunked(self):
        '''Check if the response uses chunked transfer encoding.

        Only use chunked responses when the client is speaking HTTP/1.1
        or newer and there was no Content-Length header set.
        '''
        if (self.version <= (1, 0) or
                self._status == '200 Connection established' or
                has_empty_content(int(self.status[:3]))):
            return False
        elif self.headers.get('Transfer-Encoding') == 'chunked':
            return True
        else:
            return self.content_length is None

    def get_headers(self):
        '''Get the headers to send to the client.
        '''
        if not self._status:
            # we are sending headers but the start_response was not called
            raise HttpException('Headers not set.')
        headers = self.headers
        # Set chunked header if needed
        if self.is_chunked():
            headers['Transfer-Encoding'] = 'chunked'
            headers.pop('content-length', None)
        else:
            headers.pop('Transfer-Encoding', None)
        if self.keep_alive:
            self.keep_alive = keep_alive_with_status(self._status, headers)
        if not self.keep_alive:
            headers['connection'] = 'close'
        return headers

    def wsgi_environ(self):
        #return a the WSGI environ dictionary
        transport = self.transport
        https = True if is_tls(transport.get_extra_info('socket')) else False
        multiprocess = (self.cfg.concurrency == 'process')
        environ = wsgi_environ(self._stream,
                               transport.get_extra_info('sockname'),
                               self.address, self.headers,
                               self.SERVER_SOFTWARE,
                               https=https,
                               extra={'pulsar.connection': self.connection,
                                      'pulsar.cfg': self.cfg,
                                      'wsgi.multiprocess': multiprocess})
        self.keep_alive = keep_alive(self.headers, self.parser.get_version())
        self.headers.update([('Server', self.SERVER_SOFTWARE),
                             ('Date', format_date_time(time.time()))])
        return environ

    def _new_request(self, response):
        connection = response._connection
        if not connection.closed:
            connection.data_received(response._buffer)
            return connection._current_consumer
        return response
Esempio n. 4
0
class HttpServerResponse(ProtocolConsumer):
    '''Server side WSGI :class:`.ProtocolConsumer`.

    .. attribute:: wsgi_callable

        The wsgi callable handling requests.
    '''
    _status = None
    _headers_sent = None
    _body_reader = None
    _buffer = None
    _logger = LOGGER
    SERVER_SOFTWARE = pulsar.SERVER_SOFTWARE
    ONE_TIME_EVENTS = ProtocolConsumer.ONE_TIME_EVENTS + ('on_headers',)

    def __init__(self, wsgi_callable, cfg, server_software=None, loop=None):
        super().__init__(loop=loop)
        self.wsgi_callable = wsgi_callable
        self.cfg = cfg
        self.parser = http_parser(kind=0)
        self.headers = Headers()
        self.keep_alive = False
        self.SERVER_SOFTWARE = server_software or self.SERVER_SOFTWARE

    @property
    def headers_sent(self):
        '''Available once the headers have been sent to the client.

        These are the bytes representing the first response line and
        the headers
        '''
        return self._headers_sent

    def data_received(self, data):
        '''Implements :meth:`~.ProtocolConsumer.data_received` method.

        Once we have a full HTTP message, build the wsgi ``environ`` and
        delegate the response to the :func:`wsgi_callable` function.
        '''
        parser = self.parser
        processed = parser.execute(data, len(data))
        if parser.is_headers_complete():
            if not self._body_reader:
                headers = Headers(parser.get_headers(), kind='client')
                self._body_reader = HttpBodyReader(headers,
                                                   parser,
                                                   self.transport,
                                                   loop=self._loop)
                self._response(self.wsgi_environ())
            body = parser.recv_body()
            if body:
                self._body_reader.feed_data(body)
        #
        if parser.is_message_complete():
            #
            self._body_reader.feed_eof()

            if processed < len(data):
                if not self._buffer:
                    self._buffer = data[processed:]
                    self.bind_event('post_request', self._new_request)
                else:
                    self._buffer += data[processed:]
        #
        elif processed < len(data):
            # This is a parsing error, the client must have sent
            # bogus data
            raise ProtocolError

    @property
    def status(self):
        return self._status

    @property
    def upgrade(self):
        return self.headers.get('upgrade')

    @property
    def chunked(self):
        return self.headers.get('Transfer-Encoding') == 'chunked'

    @property
    def content_length(self):
        c = self.headers.get('Content-Length')
        if c:
            return int(c)

    @property
    def version(self):
        return self.parser.get_version()

    def start_response(self, status, response_headers, exc_info=None):
        '''WSGI compliant ``start_response`` callable, see pep3333_.

        The application may call start_response more than once, if and only
        if the ``exc_info`` argument is provided.
        More precisely, it is a fatal error to call ``start_response`` without
        the ``exc_info`` argument if start_response has already been called
        within the current invocation of the application.

        :parameter status: an HTTP ``status`` string like ``200 OK`` or
            ``404 Not Found``.
        :parameter response_headers: a list of ``(header_name, header_value)``
            tuples. It must be a Python list. Each header_name must be a valid
            HTTP header field-name (as defined by RFC 2616_, Section 4.2),
            without a trailing colon or other punctuation.
        :parameter exc_info: optional python ``sys.exc_info()`` tuple.
            This argument should be supplied by the application only if
            ``start_response`` is being called by an error handler.
        :return: The :meth:`write` method.

        ``HOP_HEADERS`` are not considered but no error is raised.

        .. _pep3333: http://www.python.org/dev/peps/pep-3333/
        .. _2616: http://www.faqs.org/rfcs/rfc2616.html
        '''
        if exc_info:
            try:
                if self._headers_sent:
                    # if exc_info is provided, and the HTTP headers have
                    # already been sent, start_response must raise an error,
                    # and should re-raise using the exc_info tuple
                    reraise(*exc_info)
            finally:
                # Avoid circular reference
                exc_info = None
        elif self._status:
            # Headers already set. Raise error
            raise HttpException("Response headers already set!")
        self._status = status
        if type(response_headers) is not list:
            raise TypeError("Headers must be a list of name/value tuples")
        for header, value in response_headers:
            if header.lower() in HOP_HEADERS:
                # These features are the exclusive province of this class,
                # this should be considered a fatal error for an application
                # to attempt sending them, but we don't raise an error,
                # just log a warning
                self.logger.warning('Application passing hop header "%s"',
                                    header)
                continue
            self.headers.add_header(header, value)
        return self.write

    def write(self, data, force=False):
        '''The write function returned by the :meth:`start_response` method.

        Required by the WSGI specification.

        :param data: bytes to write
        :param force: Optional flag used internally
        :return: a :class:`~asyncio.Future` or the number of bytes written
        '''
        write = super().write
        chunks = []
        if not self._headers_sent:
            tosend = self.get_headers()
            self._headers_sent = tosend.flat(self.version, self.status)
            self.fire_event('on_headers')
            chunks.append(self._headers_sent)
        if data:
            if self.chunked:
                chunks.extend(http_chunks(data))
            else:
                chunks.append(data)
        elif force and self.chunked:
            chunks.extend(http_chunks(data, True))
        if chunks:
            return write(b''.join(chunks))

    ########################################################################
    #    INTERNALS
    @task
    def _response(self, environ):
        exc_info = None
        response = None
        done = False
        alive = self.cfg.keep_alive or 15
        while not done:
            done = True
            try:
                if exc_info is None:
                    if (not environ.get('HTTP_HOST') and
                            environ['SERVER_PROTOCOL'] != 'HTTP/1.0'):
                        raise BadRequest
                    response = self.wsgi_callable(environ, self.start_response)
                    if isfuture(response):
                        response = yield from wait_for(response, alive)
                else:
                    response = handle_wsgi_error(environ, exc_info)
                    if isfuture(response):
                        response = yield from wait_for(response, alive)
                #
                if exc_info:
                    self.start_response(response.status,
                                        response.get_headers(), exc_info)
                #
                # Do the actual writing
                loop = self._loop
                start = loop.time()
                for chunk in response:
                    if isfuture(chunk):
                        chunk = yield from wait_for(chunk, alive)
                        start = loop.time()
                    result = self.write(chunk)
                    if isfuture(result):
                        yield from wait_for(result, alive)
                        start = loop.time()
                    else:
                        time_in_loop = loop.time() - start
                        if time_in_loop > MAX_TIME_IN_LOOP:
                            self.logger.debug(
                                'Released the event loop after %.3f seconds',
                                time_in_loop)
                            yield None
                            start = loop.time()
                #
                # make sure we write headers and last chunk if needed
                self.write(b'', True)

            # client disconnected, end this connection
            except (IOError, AbortWsgi):
                self.finished()
            except Exception:
                if wsgi_request(environ).cache.handle_wsgi_error:
                    self.keep_alive = False
                    self._write_headers()
                    self.connection.close()
                    self.finished()
                else:
                    done = False
                    exc_info = sys.exc_info()
            else:
                log_wsgi_info(self.logger.info, environ, self.status)
                self.finished()
                if not self.keep_alive:
                    self.logger.debug('No keep alive, closing connection %s',
                                      self.connection)
                    self.connection.close()
            finally:
                if hasattr(response, 'close'):
                    try:
                        response.close()
                    except Exception:
                        self.logger.exception(
                            'Error while closing wsgi iterator')

    def is_chunked(self):
        '''Check if the response uses chunked transfer encoding.

        Only use chunked responses when the client is speaking HTTP/1.1
        or newer and there was no Content-Length header set.
        '''
        if (self.version <= (1, 0) or
                self._status == '200 Connection established' or
                has_empty_content(int(self.status[:3]))):
            return False
        elif self.headers.get('Transfer-Encoding') == 'chunked':
            return True
        else:
            return self.content_length is None

    def get_headers(self):
        '''Get the headers to send to the client.
        '''
        if not self._status:
            # we are sending headers but the start_response was not called
            raise HttpException('Headers not set.')
        headers = self.headers
        # Set chunked header if needed
        if self.is_chunked():
            headers['Transfer-Encoding'] = 'chunked'
            headers.pop('content-length', None)
        else:
            headers.pop('Transfer-Encoding', None)
        if self.keep_alive:
            self.keep_alive = keep_alive_with_status(self._status, headers)
        if not self.keep_alive:
            headers['connection'] = 'close'
        return headers

    def wsgi_environ(self):
        # return a the WSGI environ dictionary
        transport = self.transport
        https = True if is_tls(transport.get_extra_info('socket')) else False
        multiprocess = (self.cfg.concurrency == 'process')
        environ = wsgi_environ(self._body_reader,
                               self.parser,
                               self._body_reader.headers,
                               transport.get_extra_info('sockname'),
                               self.address,
                               self.headers,
                               self.SERVER_SOFTWARE,
                               https=https,
                               extra={'pulsar.connection': self.connection,
                                      'pulsar.cfg': self.cfg,
                                      'wsgi.multiprocess': multiprocess})
        self.keep_alive = keep_alive(self.headers, self.parser.get_version(),
                                     environ['REQUEST_METHOD'])
        self.headers.update([('Server', self.SERVER_SOFTWARE),
                             ('Date', format_date_time(time.time()))])
        return environ

    def _new_request(self, _, exc=None):
        connection = self._connection
        connection.data_received(self._buffer)

    def _write_headers(self):
        if not self._headers_sent:
            if self.content_length:
                self.headers['Content-Length'] = '0'
            self.write(b'')
Esempio n. 5
0
class HttpServerResponse(ProtocolConsumer):
    """Server side WSGI :class:`.ProtocolConsumer`.

    .. attribute:: wsgi_callable

        The wsgi callable handling requests.
    """

    _status = None
    _headers_sent = None
    _stream = None
    _buffer = None
    _logger = LOGGER
    SERVER_SOFTWARE = pulsar.SERVER_SOFTWARE
    ONE_TIME_EVENTS = ProtocolConsumer.ONE_TIME_EVENTS + ("on_headers",)

    def __init__(self, wsgi_callable, cfg, server_software=None, loop=None):
        super().__init__(loop=loop)
        self.wsgi_callable = wsgi_callable
        self.cfg = cfg
        self.parser = http_parser(kind=0)
        self.headers = Headers()
        self.keep_alive = False
        self.SERVER_SOFTWARE = server_software or self.SERVER_SOFTWARE

    @property
    def headers_sent(self):
        """Available once the headers have been sent to the client.

        These are the bytes representing the first response line and
        the headers
        """
        return self._headers_sent

    def data_received(self, data):
        """Implements :meth:`~.ProtocolConsumer.data_received` method.

        Once we have a full HTTP message, build the wsgi ``environ`` and
        delegate the response to the :func:`wsgi_callable` function.
        """
        parser = self.parser
        processed = parser.execute(data, len(data))
        if not self._stream and parser.is_headers_complete():
            headers = Headers(parser.get_headers(), kind="client")
            self._stream = StreamReader(headers, parser, self.transport)
            self._response(self.wsgi_environ())
        #
        if parser.is_message_complete():
            #
            # Stream has the whole body
            if not self._stream.on_message_complete.done():
                self._stream.on_message_complete.set_result(None)

            if processed < len(data):
                if not self._buffer:
                    self._buffer = data[processed:]
                    self.bind_event("post_request", self._new_request)
                else:
                    self._buffer += data[processed:]
        #
        elif processed < len(data):
            # This is a parsing error, the client must have sent
            # bogus data
            raise ProtocolError

    @property
    def status(self):
        return self._status

    @property
    def upgrade(self):
        return self.headers.get("upgrade")

    @property
    def chunked(self):
        return self.headers.get("Transfer-Encoding") == "chunked"

    @property
    def content_length(self):
        c = self.headers.get("Content-Length")
        if c:
            return int(c)

    @property
    def version(self):
        return self.parser.get_version()

    def start_response(self, status, response_headers, exc_info=None):
        """WSGI compliant ``start_response`` callable, see pep3333_.

        The application may call start_response more than once, if and only
        if the ``exc_info`` argument is provided.
        More precisely, it is a fatal error to call ``start_response`` without
        the ``exc_info`` argument if start_response has already been called
        within the current invocation of the application.

        :parameter status: an HTTP ``status`` string like ``200 OK`` or
            ``404 Not Found``.
        :parameter response_headers: a list of ``(header_name, header_value)``
            tuples. It must be a Python list. Each header_name must be a valid
            HTTP header field-name (as defined by RFC 2616_, Section 4.2),
            without a trailing colon or other punctuation.
        :parameter exc_info: optional python ``sys.exc_info()`` tuple.
            This argument should be supplied by the application only if
            ``start_response`` is being called by an error handler.
        :return: The :meth:`write` method.

        ``HOP_HEADERS`` are not considered but no error is raised.

        .. _pep3333: http://www.python.org/dev/peps/pep-3333/
        .. _2616: http://www.faqs.org/rfcs/rfc2616.html
        """
        if exc_info:
            try:
                if self._headers_sent:
                    # if exc_info is provided, and the HTTP headers have
                    # already been sent, start_response must raise an error,
                    # and should re-raise using the exc_info tuple
                    reraise(*exc_info)
            finally:
                # Avoid circular reference
                exc_info = None
        elif self._status:
            # Headers already set. Raise error
            raise HttpException("Response headers already set!")
        self._status = status
        if type(response_headers) is not list:
            raise TypeError("Headers must be a list of name/value tuples")
        for header, value in response_headers:
            if header.lower() in HOP_HEADERS:
                # These features are the exclusive province of this class,
                # this should be considered a fatal error for an application
                # to attempt sending them, but we don't raise an error,
                # just log a warning
                self.logger.warning('Application passing hop header "%s"', header)
                continue
            self.headers.add_header(header, value)
        return self.write

    def write(self, data, force=False):
        """The write function returned by the :meth:`start_response` method.

        Required by the WSGI specification.

        :param data: bytes to write
        :param force: Optional flag used internally
        :return: a :class:`~asyncio.Future` or the number of bytes written
        """
        write = super().write
        chunks = []
        if not self._headers_sent:
            tosend = self.get_headers()
            self._headers_sent = tosend.flat(self.version, self.status)
            self.fire_event("on_headers")
            chunks.append(self._headers_sent)
        if data:
            if self.chunked:
                while len(data) >= MAX_CHUNK_SIZE:
                    chunk, data = data[:MAX_CHUNK_SIZE], data[MAX_CHUNK_SIZE:]
                    chunks.append(chunk_encoding(chunk))
                if data:
                    chunks.append(chunk_encoding(data))
            else:
                chunks.append(data)
        elif force and self.chunked:
            chunks.append(chunk_encoding(data))
        if chunks:
            return write(b"".join(chunks))

    ########################################################################
    #    INTERNALS
    @task
    def _response(self, environ):
        exc_info = None
        response = None
        done = False
        alive = self.cfg.keep_alive or 15
        while not done:
            done = True
            try:
                if exc_info is None:
                    if "SERVER_NAME" not in environ:
                        raise HttpException(status=400)
                    response = self.wsgi_callable(environ, self.start_response)
                    if isfuture(response):
                        response = yield from wait_for(response, alive)
                else:
                    response = handle_wsgi_error(environ, exc_info)
                    if isfuture(response):
                        response = yield from wait_for(response, alive)
                #
                if exc_info:
                    self.start_response(response.status, response.get_headers(), exc_info)
                #
                # Do the actual writing
                loop = self._loop
                start = loop.time()
                for chunk in response:
                    if isfuture(chunk):
                        chunk = yield from wait_for(chunk, alive)
                        start = loop.time()
                    result = self.write(chunk)
                    if isfuture(result):
                        yield from wait_for(result, alive)
                        start = loop.time()
                    else:
                        time_in_loop = loop.time() - start
                        if time_in_loop > MAX_TIME_IN_LOOP:
                            self.logger.debug("Released the event loop after %.3f seconds", time_in_loop)
                            yield None
                            start = loop.time()
                #
                # make sure we write headers and last chunk if needed
                self.write(b"", True)

            except IOError:  # client disconnected, end this connection
                self.finished()
            except Exception:
                if wsgi_request(environ).cache.handle_wsgi_error:
                    self.keep_alive = False
                    self._write_headers()
                    self.connection.close()
                    self.finished()
                else:
                    done = False
                    exc_info = sys.exc_info()
            else:
                if not self.keep_alive:
                    self.connection.close()
                self.finished()
                log_wsgi_info(self.logger.info, environ, self.status)
            finally:
                if hasattr(response, "close"):
                    try:
                        response.close()
                    except Exception:
                        self.logger.exception("Error while closing wsgi iterator")

    def is_chunked(self):
        """Check if the response uses chunked transfer encoding.

        Only use chunked responses when the client is speaking HTTP/1.1
        or newer and there was no Content-Length header set.
        """
        if (
            self.version <= (1, 0)
            or self._status == "200 Connection established"
            or has_empty_content(int(self.status[:3]))
        ):
            return False
        elif self.headers.get("Transfer-Encoding") == "chunked":
            return True
        else:
            return self.content_length is None

    def get_headers(self):
        """Get the headers to send to the client.
        """
        if not self._status:
            # we are sending headers but the start_response was not called
            raise HttpException("Headers not set.")
        headers = self.headers
        # Set chunked header if needed
        if self.is_chunked():
            headers["Transfer-Encoding"] = "chunked"
            headers.pop("content-length", None)
        else:
            headers.pop("Transfer-Encoding", None)
        if self.keep_alive:
            self.keep_alive = keep_alive_with_status(self._status, headers)
        if not self.keep_alive:
            headers["connection"] = "close"
        return headers

    def wsgi_environ(self):
        # return a the WSGI environ dictionary
        transport = self.transport
        https = True if is_tls(transport.get_extra_info("socket")) else False
        multiprocess = self.cfg.concurrency == "process"
        environ = wsgi_environ(
            self._stream,
            self.parser,
            self._stream.headers,
            transport.get_extra_info("sockname"),
            self.address,
            self.headers,
            self.SERVER_SOFTWARE,
            https=https,
            extra={"pulsar.connection": self.connection, "pulsar.cfg": self.cfg, "wsgi.multiprocess": multiprocess},
        )
        self.keep_alive = keep_alive(self.headers, self.parser.get_version())
        self.headers.update([("Server", self.SERVER_SOFTWARE), ("Date", format_date_time(time.time()))])
        return environ

    def _new_request(self, _, exc=None):
        connection = self._connection
        connection.data_received(self._buffer)

    def _write_headers(self):
        if not self._headers_sent:
            if self.content_length:
                self.headers["Content-Length"] = "0"
            self.write(b"")
Esempio n. 6
0
class HttpServerResponse(ProtocolConsumer):
    '''Server side WSGI :class:`.ProtocolConsumer`.

    .. attribute:: wsgi_callable

        The wsgi callable handling requests.
    '''
    _status = None
    _headers_sent = None
    _request_headers = None
    SERVER_SOFTWARE = pulsar.SERVER_SOFTWARE
    ONE_TIME_EVENTS = ProtocolConsumer.ONE_TIME_EVENTS + ('on_headers', )

    def __init__(self, wsgi_callable, cfg, server_software=None):
        super(HttpServerResponse, self).__init__()
        self.wsgi_callable = wsgi_callable
        self.cfg = cfg
        self.parser = http_parser(kind=0)
        self.headers = Headers()
        self.keep_alive = False
        self.SERVER_SOFTWARE = server_software or self.SERVER_SOFTWARE

    def data_received(self, data):
        '''Implements :meth:`~.ProtocolConsumer.data_received` method.

        Once we have a full HTTP message, build the wsgi ``environ`` and
        delegate the response to the :func:`wsgi_callable` function.
        '''
        p = self.parser
        if p.execute(bytes(data), len(data)) == len(data):
            if self._request_headers is None and p.is_headers_complete():
                self._request_headers = Headers(p.get_headers(), kind='client')
                stream = StreamReader(self._request_headers, p, self.transport)
                self.bind_event('data_processed', stream.data_processed)
                environ = self.wsgi_environ(stream)
                self.event_loop. async (self._response(environ))
        else:
            # This is a parsing error, the client must have sent
            # bogus data
            raise ProtocolError

    @property
    def status(self):
        return self._status

    @property
    def upgrade(self):
        return self.headers.get('upgrade')

    @property
    def chunked(self):
        return self.headers.get('Transfer-Encoding') == 'chunked'

    @property
    def content_length(self):
        c = self.headers.get('Content-Length')
        if c:
            return int(c)

    @property
    def version(self):
        return self.parser.get_version()

    def start_response(self, status, response_headers, exc_info=None):
        '''WSGI compliant ``start_response`` callable, see pep3333_.

        The application may call start_response more than once, if and only
        if the ``exc_info`` argument is provided.
        More precisely, it is a fatal error to call ``start_response`` without
        the ``exc_info`` argument if start_response has already been called
        within the current invocation of the application.

        :parameter status: an HTTP ``status`` string like ``200 OK`` or
            ``404 Not Found``.
        :parameter response_headers: a list of ``(header_name, header_value)``
            tuples. It must be a Python list. Each header_name must be a valid
            HTTP header field-name (as defined by RFC 2616_, Section 4.2),
            without a trailing colon or other punctuation.
        :parameter exc_info: optional python ``sys.exc_info()`` tuple.
            This argument should be supplied by the application only if
            ``start_response`` is being called by an error handler.
        :return: The :meth:`write` method.

        ``HOP_HEADERS`` are not considered but no error is raised.

        .. _pep3333: http://www.python.org/dev/peps/pep-3333/
        .. _2616: http://www.faqs.org/rfcs/rfc2616.html
        '''
        if exc_info:
            try:
                if self._headers_sent:
                    # if exc_info is provided, and the HTTP headers have
                    # already been sent, start_response must raise an error,
                    # and should re-raise using the exc_info tuple
                    raise_error_trace(exc_info[1], exc_info[2])
            finally:
                # Avoid circular reference
                exc_info = None
        elif self._status:
            # Headers already set. Raise error
            raise HttpException("Response headers already set!")
        self._status = status
        if type(response_headers) is not list:
            raise TypeError("Headers must be a list of name/value tuples")
        for header, value in response_headers:
            if header.lower() in HOP_HEADERS:
                # These features are the exclusive province of this class,
                # this should be considered a fatal error for an application
                # to attempt sending them, but we don't raise an error,
                # just log a warning
                LOGGER.warning('Application handler passing hop header "%s"',
                               header)
                continue
            self.headers.add_header(header, value)
        return self.write

    def write(self, data, force=False):
        '''The write function returned by the :meth:`start_response` method.

        Required by the WSGI specification.

        :param data: bytes to write
        :param force: Optional flag used internally.
        '''
        if not self._headers_sent:
            tosend = self.get_headers()
            self._headers_sent = tosend.flat(self.version, self.status)
            self.fire_event('on_headers')
            self.transport.write(self._headers_sent)
        if data:
            if self.chunked:
                chunks = []
                while len(data) >= MAX_CHUNK_SIZE:
                    chunk, data = data[:MAX_CHUNK_SIZE], data[MAX_CHUNK_SIZE:]
                    chunks.append(chunk_encoding(chunk))
                if data:
                    chunks.append(chunk_encoding(data))
                self.transport.write(b''.join(chunks))
            else:
                self.transport.write(data)
        elif force and self.chunked:
            self.transport.write(chunk_encoding(data))

    ########################################################################
    ##    INTERNALS
    def _response(self, environ):
        exc_info = None
        try:
            if 'SERVER_NAME' not in environ:
                raise HttpException(status=400)
            wsgi_iter = self.wsgi_callable(environ, self.start_response)
            yield self._async_wsgi(wsgi_iter)
        except IOError:  # client disconnected, end this connection
            self.finished()
        except Exception:
            exc_info = sys.exc_info()
        if exc_info:
            failure = Failure(exc_info)
            try:
                wsgi_iter = handle_wsgi_error(environ, failure)
                self.start_response(wsgi_iter.status, wsgi_iter.get_headers(),
                                    exc_info)
                yield self._async_wsgi(wsgi_iter)
            except Exception:
                # Error handling did not work, Just shut down
                self.keep_alive = False
                self.finish_wsgi()

    def _async_wsgi(self, wsgi_iter):
        if isinstance(wsgi_iter, (Deferred, Failure)):
            wsgi_iter = yield wsgi_iter
        try:
            for b in wsgi_iter:
                chunk = yield b  # handle asynchronous components
                self.write(chunk)
            # make sure we write headers
            self.write(b'', True)
        finally:
            if hasattr(wsgi_iter, 'close'):
                try:
                    wsgi_iter.close()
                except Exception:
                    LOGGER.exception('Error while closing wsgi iterator')
        self.finish_wsgi()

    def finish_wsgi(self):
        if not self.keep_alive:
            self.connection.close()
        self.finished()

    def is_chunked(self):
        '''Check if the response uses chunked transfer encoding.

        Only use chunked responses when the client is speaking HTTP/1.1
        or newer and there was no Content-Length header set.
        '''
        if (self.version <= (1, 0)
                or self._status == '200 Connection established'
                or has_empty_content(int(self.status[:3]))):
            return False
        elif self.headers.get('Transfer-Encoding') == 'chunked':
            return True
        else:
            return self.content_length is None

    def get_headers(self):
        '''Get the headers to send to the client.
        '''
        if not self._status:
            # we are sending headers but the start_response was not called
            raise HttpException('Headers not set.')
        headers = self.headers
        # Set chunked header if needed
        if self.is_chunked():
            headers['Transfer-Encoding'] = 'chunked'
            headers.pop('content-length', None)
        else:
            headers.pop('Transfer-Encoding', None)
        if self.keep_alive:
            self.keep_alive = keep_alive_with_status(self._status, headers)
        if not self.keep_alive:
            headers['connection'] = 'close'
        # If client sent cookies and set-cookies header is not available
        # set the cookies
        if 'cookie' in self._request_headers and not 'set-cookie' in headers:
            headers['Set-cookie'] = self._request_headers['cookie']
        return headers

    def wsgi_environ(self, stream):
        #return a the WSGI environ dictionary
        parser = self.parser
        https = True if is_tls(self.transport.sock) else False
        multiprocess = (self.cfg.concurrency == 'process')
        environ = wsgi_environ(stream,
                               self.transport.address,
                               self.address,
                               self._request_headers,
                               self.headers,
                               self.SERVER_SOFTWARE,
                               https=https,
                               extra={
                                   'pulsar.connection': self.connection,
                                   'pulsar.cfg': self.cfg,
                                   'wsgi.multiprocess': multiprocess
                               })
        self.keep_alive = keep_alive(self.headers, parser.get_version())
        self.headers.update([('Server', self.SERVER_SOFTWARE),
                             ('Date', format_date_time(time.time()))])
        return environ