mirror of
				https://github.com/django/daphne.git
				synced 2025-10-31 07:47:25 +03:00 
			
		
		
		
	
		
			
				
	
	
		
			255 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			255 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from __future__ import unicode_literals
 | |
| 
 | |
| import sys
 | |
| import logging
 | |
| from threading import Lock
 | |
| 
 | |
| from django import http
 | |
| from django.core import signals
 | |
| from django.core.handlers import base
 | |
| from django.core.urlresolvers import set_script_prefix
 | |
| from django.utils import six
 | |
| from django.utils.functional import cached_property
 | |
| 
 | |
| 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.
 | |
|     """
 | |
| 
 | |
|     # Exception that will cause any handler to skip around response
 | |
|     # transmission and presume something else will do it later.
 | |
|     class ResponseLater(Exception):
 | |
|         pass
 | |
| 
 | |
|     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
 | |
|         # TODO: probably needs actual URL decoding
 | |
|         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"),
 | |
|             # 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]
 | |
|         # Headers go into META
 | |
|         for name, value in self.message.get('headers', {}).items():
 | |
|             if name == "content-length":
 | |
|                 corrected_name = "CONTENT_LENGTH"
 | |
|             elif name == "content-type":
 | |
|                 corrected_name = "CONTENT_TYPE"
 | |
|             else:
 | |
|                 corrected_name = 'HTTP_%s' % name.upper().replace("-", "_")
 | |
|             # TODO: Look at request encoding for unicode decode
 | |
|             self.META[corrected_name] = value.decode("latin1")
 | |
|         # 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"
 | |
|         # 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()
 | |
|         except AsgiRequest.ResponseLater:
 | |
|             # The view has promised something else
 | |
|             # will send a response at a later time
 | |
|             return
 | |
|         else:
 | |
|             response = self.get_response(request)
 | |
|         # Transform response into messages, which we yield back to caller
 | |
|         for message in self.encode_response(response):
 | |
|             # TODO: file_to_stream
 | |
|             yield message
 | |
| 
 | |
|     @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.binary_type):
 | |
|                 header = header.decode("latin1")
 | |
|             if isinstance(value, six.text_type):
 | |
|                 value = value.encode("latin1")
 | |
|             response_headers.append(
 | |
|                 (
 | |
|                     six.text_type(header),
 | |
|                     six.binary_type(value),
 | |
|                 )
 | |
|             )
 | |
|         for c in response.cookies.values():
 | |
|             response_headers.append(
 | |
|                 (
 | |
|                     'Set-Cookie',
 | |
|                     c.output(header='').encode("ascii"),
 | |
|                 )
 | |
|             )
 | |
|         # Make initial response message
 | |
|         message = {
 | |
|             "status": response.status_code,
 | |
|             "status_text": response.reason_phrase.encode("ascii"),
 | |
|             "headers": response_headers,
 | |
|         }
 | |
|         # Streaming responses need to be pinned to their iterator
 | |
|         if response.streaming:
 | |
|             for part in response.streaming_content:
 | |
|                 for chunk in cls.chunk_bytes(part):
 | |
|                     message['content'] = chunk
 | |
|                     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
 | |
|         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)
 |