mirror of
https://github.com/django/daphne.git
synced 2025-06-23 14:33:24 +03:00
318 lines
11 KiB
Python
318 lines
11 KiB
Python
from __future__ import unicode_literals
|
|
|
|
import cgi
|
|
import codecs
|
|
import logging
|
|
import sys
|
|
import traceback
|
|
from io import BytesIO
|
|
from threading import Lock
|
|
|
|
from django import http
|
|
from django.conf import settings
|
|
from django.core import signals
|
|
from django.core.handlers import base
|
|
from django.core.urlresolvers import set_script_prefix
|
|
from django.http import FileResponse, HttpResponseServerError
|
|
from django.utils import six
|
|
from django.utils.functional import cached_property
|
|
|
|
from .exceptions import ResponseLater as ResponseLaterOuter
|
|
|
|
logger = logging.getLogger('django.request')
|
|
|
|
|
|
class AsgiRequest(http.HttpRequest):
|
|
"""
|
|
Custom request subclass that decodes from an ASGI-standard request
|
|
dict, and wraps request body handling.
|
|
"""
|
|
|
|
ResponseLater = ResponseLaterOuter
|
|
|
|
def __init__(self, message):
|
|
self.message = message
|
|
self.reply_channel = self.message.reply_channel
|
|
self._content_length = 0
|
|
self._post_parse_error = False
|
|
self.resolver_match = None
|
|
# Path info
|
|
self.path = self.message['path'].decode("ascii")
|
|
self.script_name = self.message.get('root_path', b'')
|
|
if self.script_name:
|
|
# TODO: Better is-prefix checking, slash handling?
|
|
self.path_info = self.path[len(self.script_name):]
|
|
else:
|
|
self.path_info = self.path
|
|
# HTTP basics
|
|
self.method = self.message['method'].upper()
|
|
self.META = {
|
|
"REQUEST_METHOD": self.method,
|
|
"QUERY_STRING": self.message.get('query_string', b'').decode("ascii"),
|
|
"SCRIPT_NAME": self.script_name.decode("ascii"),
|
|
# Old code will need these for a while
|
|
"wsgi.multithread": True,
|
|
"wsgi.multiprocess": True,
|
|
}
|
|
if self.message.get('client', None):
|
|
self.META['REMOTE_ADDR'] = self.message['client'][0]
|
|
self.META['REMOTE_HOST'] = self.META['REMOTE_ADDR']
|
|
self.META['REMOTE_PORT'] = self.message['client'][1]
|
|
if self.message.get('server', None):
|
|
self.META['SERVER_NAME'] = self.message['server'][0]
|
|
self.META['SERVER_PORT'] = self.message['server'][1]
|
|
# Handle old style-headers for a transition period
|
|
if "headers" in self.message and isinstance(self.message['headers'], dict):
|
|
self.message['headers'] = [
|
|
(x.encode("latin1"), y) for x, y in
|
|
self.message['headers'].items()
|
|
]
|
|
# Headers go into META
|
|
for name, value in self.message.get('headers', []):
|
|
name = name.decode("latin1")
|
|
if name == "content-length":
|
|
corrected_name = "CONTENT_LENGTH"
|
|
elif name == "content-type":
|
|
corrected_name = "CONTENT_TYPE"
|
|
else:
|
|
corrected_name = 'HTTP_%s' % name.upper().replace("-", "_")
|
|
# HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in case
|
|
value = value.decode("latin1")
|
|
if corrected_name in self.META:
|
|
value = self.META[corrected_name] + "," + value.decode("latin1")
|
|
self.META[corrected_name] = value
|
|
# Pull out request encoding if we find it
|
|
if "CONTENT_TYPE" in self.META:
|
|
_, content_params = cgi.parse_header(self.META["CONTENT_TYPE"])
|
|
if 'charset' in content_params:
|
|
try:
|
|
codecs.lookup(content_params['charset'])
|
|
except LookupError:
|
|
pass
|
|
else:
|
|
self.encoding = content_params['charset']
|
|
# Pull out content length info
|
|
if self.META.get('CONTENT_LENGTH', None):
|
|
try:
|
|
self._content_length = int(self.META['CONTENT_LENGTH'])
|
|
except (ValueError, TypeError):
|
|
pass
|
|
# Body handling
|
|
self._body = message.get("body", b"")
|
|
if message.get("body_channel", None):
|
|
while True:
|
|
# Get the next chunk from the request body channel
|
|
chunk = None
|
|
while chunk is None:
|
|
_, chunk = message.channel_layer.receive_many(
|
|
[message['body_channel']],
|
|
block=True,
|
|
)
|
|
# Add content to body
|
|
self._body += chunk.get("content", "")
|
|
# Exit loop if this was the last
|
|
if not chunk.get("more_content", False):
|
|
break
|
|
assert isinstance(self._body, six.binary_type), "Body is not bytes"
|
|
# Add a stream-a-like for the body
|
|
self._stream = BytesIO(self._body)
|
|
# Other bits
|
|
self.resolver_match = None
|
|
|
|
@cached_property
|
|
def GET(self):
|
|
return http.QueryDict(
|
|
self.message.get('query_string', ''),
|
|
encoding=self._encoding,
|
|
)
|
|
|
|
def _get_post(self):
|
|
if not hasattr(self, '_post'):
|
|
self._read_started = False
|
|
self._load_post_and_files()
|
|
return self._post
|
|
|
|
def _set_post(self, post):
|
|
self._post = post
|
|
|
|
def _get_files(self):
|
|
if not hasattr(self, '_files'):
|
|
self._load_post_and_files()
|
|
return self._files
|
|
|
|
POST = property(_get_post, _set_post)
|
|
FILES = property(_get_files)
|
|
|
|
@cached_property
|
|
def COOKIES(self):
|
|
return http.parse_cookie(self.META.get('HTTP_COOKIE', ''))
|
|
|
|
|
|
class AsgiHandler(base.BaseHandler):
|
|
"""
|
|
Handler for ASGI requests for the view system only (it will have got here
|
|
after traversing the dispatch-by-channel-name system, which decides it's
|
|
a HTTP request)
|
|
"""
|
|
|
|
initLock = Lock()
|
|
request_class = AsgiRequest
|
|
|
|
# Size to chunk response bodies into for multiple response messages
|
|
chunk_size = 512 * 1024
|
|
|
|
def __call__(self, message):
|
|
# Set up middleware if needed. We couldn't do this earlier, because
|
|
# settings weren't available.
|
|
if self._request_middleware is None:
|
|
with self.initLock:
|
|
# Check that middleware is still uninitialized.
|
|
if self._request_middleware is None:
|
|
self.load_middleware()
|
|
# Set script prefix from message root_path
|
|
set_script_prefix(message.get('root_path', ''))
|
|
signals.request_started.send(sender=self.__class__, message=message)
|
|
# Run request through view system
|
|
try:
|
|
request = self.request_class(message)
|
|
except UnicodeDecodeError:
|
|
logger.warning(
|
|
'Bad Request (UnicodeDecodeError)',
|
|
exc_info=sys.exc_info(),
|
|
extra={
|
|
'status_code': 400,
|
|
}
|
|
)
|
|
response = http.HttpResponseBadRequest()
|
|
else:
|
|
try:
|
|
response = self.get_response(request)
|
|
# Fix chunk size on file responses
|
|
if isinstance(response, FileResponse):
|
|
response.block_size = 1024 * 512
|
|
except AsgiRequest.ResponseLater:
|
|
# The view has promised something else
|
|
# will send a response at a later time
|
|
return
|
|
# Transform response into messages, which we yield back to caller
|
|
for message in self.encode_response(response):
|
|
# TODO: file_to_stream
|
|
yield message
|
|
|
|
def process_exception_by_middleware(self, exception, request):
|
|
"""
|
|
Catches ResponseLater and re-raises it, else tries to delegate
|
|
to middleware exception handling.
|
|
"""
|
|
if isinstance(exception, AsgiRequest.ResponseLater):
|
|
raise
|
|
else:
|
|
return super(AsgiHandler, self).process_exception_by_middleware(exception, request)
|
|
|
|
def handle_uncaught_exception(self, request, resolver, exc_info):
|
|
"""
|
|
Propagates ResponseLater up into the higher handler method,
|
|
processes everything else
|
|
"""
|
|
# ResponseLater needs to be bubbled up the stack
|
|
if issubclass(exc_info[0], AsgiRequest.ResponseLater):
|
|
raise
|
|
# There's no WSGI server to catch the exception further up if this fails,
|
|
# so translate it into a plain text response.
|
|
try:
|
|
return super(AsgiHandler, self).handle_uncaught_exception(request, resolver, exc_info)
|
|
except:
|
|
return HttpResponseServerError(
|
|
traceback.format_exc() if settings.DEBUG else "Internal Server Error",
|
|
content_type="text/plain",
|
|
)
|
|
|
|
@classmethod
|
|
def encode_response(cls, response):
|
|
"""
|
|
Encodes a Django HTTP response into ASGI http.response message(s).
|
|
"""
|
|
# Collect cookies into headers.
|
|
# Note that we have to preserve header case as there are some non-RFC
|
|
# compliant clients that want things like Content-Type correct. Ugh.
|
|
response_headers = []
|
|
for header, value in response.items():
|
|
if isinstance(header, six.text_type):
|
|
header = header.encode("ascii")
|
|
if isinstance(value, six.text_type):
|
|
value = value.encode("latin1")
|
|
response_headers.append(
|
|
(
|
|
six.binary_type(header),
|
|
six.binary_type(value),
|
|
)
|
|
)
|
|
for c in response.cookies.values():
|
|
response_headers.append(
|
|
(
|
|
b'Set-Cookie',
|
|
c.output(header='').encode("ascii"),
|
|
)
|
|
)
|
|
# Make initial response message
|
|
message = {
|
|
"status": response.status_code,
|
|
"headers": response_headers,
|
|
}
|
|
# Streaming responses need to be pinned to their iterator
|
|
if response.streaming:
|
|
for part in response.streaming_content:
|
|
for chunk, more in cls.chunk_bytes(part):
|
|
message['content'] = chunk
|
|
# We ignore "more" as there may be more parts; instead,
|
|
# we use an empty final closing message with False.
|
|
message['more_content'] = True
|
|
yield message
|
|
message = {}
|
|
# Final closing message
|
|
yield {
|
|
"more_content": False,
|
|
}
|
|
# Other responses just need chunking
|
|
else:
|
|
# Yield chunks of response
|
|
for chunk, last in cls.chunk_bytes(response.content):
|
|
message['content'] = chunk
|
|
message['more_content'] = not last
|
|
yield message
|
|
message = {}
|
|
|
|
@classmethod
|
|
def chunk_bytes(cls, data):
|
|
"""
|
|
Chunks some data into chunks based on the current ASGI channel layer's
|
|
message size and reasonable defaults.
|
|
|
|
Yields (chunk, last_chunk) tuples.
|
|
"""
|
|
position = 0
|
|
if not data:
|
|
yield data, True
|
|
return
|
|
while position < len(data):
|
|
yield (
|
|
data[position:position + cls.chunk_size],
|
|
(position + cls.chunk_size) >= len(data),
|
|
)
|
|
position += cls.chunk_size
|
|
|
|
|
|
class ViewConsumer(object):
|
|
"""
|
|
Dispatches channel HTTP requests into django's URL/View system.
|
|
"""
|
|
|
|
handler_class = AsgiHandler
|
|
|
|
def __init__(self):
|
|
self.handler = self.handler_class()
|
|
|
|
def __call__(self, message):
|
|
for reply_message in self.handler(message):
|
|
message.reply_channel.send(reply_message)
|