def test_cookies(self): h = Headers() cookies = SimpleCookie({'bla': 'foo', 'pippo': 'pluto'}) self.assertEqual(len(cookies), 2) for c in cookies.values(): v = c.OutputString() h.add_header('Set-Cookie', v) h = str(h) self.assertTrue( h in ('Set-Cookie: bla=foo\r\nSet-Cookie: pippo=pluto\r\n\r\n', 'Set-Cookie: pippo=pluto\r\nSet-Cookie: bla=foo\r\n\r\n'))
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)
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
def test_add_header_with_params(self): h = Headers() h.add_header('content-type', 'text/html', charset=DEFAULT_CHARSET) self.assertEqual(h['content-type'], 'text/html; charset=ISO-8859-1')
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'')
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"")
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