Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/core/handlers/asgi.py: 20%

158 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import logging 

2import sys 

3import tempfile 

4import traceback 

5 

6from asgiref.sync import ThreadSensitiveContext, sync_to_async 

7 

8from django.conf import settings 

9from django.core import signals 

10from django.core.exceptions import RequestAborted, RequestDataTooBig 

11from django.core.handlers import base 

12from django.http import ( 

13 FileResponse, 

14 HttpRequest, 

15 HttpResponse, 

16 HttpResponseBadRequest, 

17 HttpResponseServerError, 

18 QueryDict, 

19 parse_cookie, 

20) 

21from django.urls import set_script_prefix 

22from django.utils.functional import cached_property 

23 

24logger = logging.getLogger("django.request") 

25 

26 

27class ASGIRequest(HttpRequest): 

28 """ 

29 Custom request subclass that decodes from an ASGI-standard request dict 

30 and wraps request body handling. 

31 """ 

32 

33 # Number of seconds until a Request gives up on trying to read a request 

34 # body and aborts. 

35 body_receive_timeout = 60 

36 

37 def __init__(self, scope, body_file): 

38 self.scope = scope 

39 self._post_parse_error = False 

40 self._read_started = False 

41 self.resolver_match = None 

42 self.script_name = self.scope.get("root_path", "") 

43 if self.script_name and scope["path"].startswith(self.script_name): 

44 # TODO: Better is-prefix checking, slash handling? 

45 self.path_info = scope["path"][len(self.script_name) :] 

46 else: 

47 self.path_info = scope["path"] 

48 # The Django path is different from ASGI scope path args, it should 

49 # combine with script name. 

50 if self.script_name: 

51 self.path = "%s/%s" % ( 

52 self.script_name.rstrip("/"), 

53 self.path_info.replace("/", "", 1), 

54 ) 

55 else: 

56 self.path = scope["path"] 

57 # HTTP basics. 

58 self.method = self.scope["method"].upper() 

59 # Ensure query string is encoded correctly. 

60 query_string = self.scope.get("query_string", "") 

61 if isinstance(query_string, bytes): 

62 query_string = query_string.decode() 

63 self.META = { 

64 "REQUEST_METHOD": self.method, 

65 "QUERY_STRING": query_string, 

66 "SCRIPT_NAME": self.script_name, 

67 "PATH_INFO": self.path_info, 

68 # WSGI-expecting code will need these for a while 

69 "wsgi.multithread": True, 

70 "wsgi.multiprocess": True, 

71 } 

72 if self.scope.get("client"): 

73 self.META["REMOTE_ADDR"] = self.scope["client"][0] 

74 self.META["REMOTE_HOST"] = self.META["REMOTE_ADDR"] 

75 self.META["REMOTE_PORT"] = self.scope["client"][1] 

76 if self.scope.get("server"): 

77 self.META["SERVER_NAME"] = self.scope["server"][0] 

78 self.META["SERVER_PORT"] = str(self.scope["server"][1]) 

79 else: 

80 self.META["SERVER_NAME"] = "unknown" 

81 self.META["SERVER_PORT"] = "0" 

82 # Headers go into META. 

83 for name, value in self.scope.get("headers", []): 

84 name = name.decode("latin1") 

85 if name == "content-length": 

86 corrected_name = "CONTENT_LENGTH" 

87 elif name == "content-type": 

88 corrected_name = "CONTENT_TYPE" 

89 else: 

90 corrected_name = "HTTP_%s" % name.upper().replace("-", "_") 

91 # HTTP/2 say only ASCII chars are allowed in headers, but decode 

92 # latin1 just in case. 

93 value = value.decode("latin1") 

94 if corrected_name in self.META: 

95 value = self.META[corrected_name] + "," + value 

96 self.META[corrected_name] = value 

97 # Pull out request encoding, if provided. 

98 self._set_content_type_params(self.META) 

99 # Directly assign the body file to be our stream. 

100 self._stream = body_file 

101 # Other bits. 

102 self.resolver_match = None 

103 

104 @cached_property 

105 def GET(self): 

106 return QueryDict(self.META["QUERY_STRING"]) 

107 

108 def _get_scheme(self): 

109 return self.scope.get("scheme") or super()._get_scheme() 

110 

111 def _get_post(self): 

112 if not hasattr(self, "_post"): 

113 self._load_post_and_files() 

114 return self._post 

115 

116 def _set_post(self, post): 

117 self._post = post 

118 

119 def _get_files(self): 

120 if not hasattr(self, "_files"): 

121 self._load_post_and_files() 

122 return self._files 

123 

124 POST = property(_get_post, _set_post) 

125 FILES = property(_get_files) 

126 

127 @cached_property 

128 def COOKIES(self): 

129 return parse_cookie(self.META.get("HTTP_COOKIE", "")) 

130 

131 

132class ASGIHandler(base.BaseHandler): 

133 """Handler for ASGI requests.""" 

134 

135 request_class = ASGIRequest 

136 # Size to chunk response bodies into for multiple response messages. 

137 chunk_size = 2**16 

138 

139 def __init__(self): 

140 super().__init__() 

141 self.load_middleware(is_async=True) 

142 

143 async def __call__(self, scope, receive, send): 

144 """ 

145 Async entrypoint - parses the request and hands off to get_response. 

146 """ 

147 # Serve only HTTP connections. 

148 # FIXME: Allow to override this. 

149 if scope["type"] != "http": 

150 raise ValueError( 

151 "Django can only handle ASGI/HTTP connections, not %s." % scope["type"] 

152 ) 

153 

154 async with ThreadSensitiveContext(): 

155 await self.handle(scope, receive, send) 

156 

157 async def handle(self, scope, receive, send): 

158 """ 

159 Handles the ASGI request. Called via the __call__ method. 

160 """ 

161 # Receive the HTTP request body as a stream object. 

162 try: 

163 body_file = await self.read_body(receive) 

164 except RequestAborted: 

165 return 

166 # Request is complete and can be served. 

167 set_script_prefix(self.get_script_prefix(scope)) 

168 await sync_to_async(signals.request_started.send, thread_sensitive=True)( 

169 sender=self.__class__, scope=scope 

170 ) 

171 # Get the request and check for basic issues. 

172 request, error_response = self.create_request(scope, body_file) 

173 if request is None: 

174 await self.send_response(error_response, send) 

175 return 

176 # Get the response, using the async mode of BaseHandler. 

177 response = await self.get_response_async(request) 

178 response._handler_class = self.__class__ 

179 # Increase chunk size on file responses (ASGI servers handles low-level 

180 # chunking). 

181 if isinstance(response, FileResponse): 

182 response.block_size = self.chunk_size 

183 # Send the response. 

184 await self.send_response(response, send) 

185 

186 async def read_body(self, receive): 

187 """Reads an HTTP body from an ASGI connection.""" 

188 # Use the tempfile that auto rolls-over to a disk file as it fills up. 

189 body_file = tempfile.SpooledTemporaryFile( 

190 max_size=settings.FILE_UPLOAD_MAX_MEMORY_SIZE, mode="w+b" 

191 ) 

192 while True: 

193 message = await receive() 

194 if message["type"] == "http.disconnect": 

195 # Early client disconnect. 

196 raise RequestAborted() 

197 # Add a body chunk from the message, if provided. 

198 if "body" in message: 

199 body_file.write(message["body"]) 

200 # Quit out if that's the end. 

201 if not message.get("more_body", False): 

202 break 

203 body_file.seek(0) 

204 return body_file 

205 

206 def create_request(self, scope, body_file): 

207 """ 

208 Create the Request object and returns either (request, None) or 

209 (None, response) if there is an error response. 

210 """ 

211 try: 

212 return self.request_class(scope, body_file), None 

213 except UnicodeDecodeError: 

214 logger.warning( 

215 "Bad Request (UnicodeDecodeError)", 

216 exc_info=sys.exc_info(), 

217 extra={"status_code": 400}, 

218 ) 

219 return None, HttpResponseBadRequest() 

220 except RequestDataTooBig: 

221 return None, HttpResponse("413 Payload too large", status=413) 

222 

223 def handle_uncaught_exception(self, request, resolver, exc_info): 

224 """Last-chance handler for exceptions.""" 

225 # There's no WSGI server to catch the exception further up 

226 # if this fails, so translate it into a plain text response. 

227 try: 

228 return super().handle_uncaught_exception(request, resolver, exc_info) 

229 except Exception: 

230 return HttpResponseServerError( 

231 traceback.format_exc() if settings.DEBUG else "Internal Server Error", 

232 content_type="text/plain", 

233 ) 

234 

235 async def send_response(self, response, send): 

236 """Encode and send a response out over ASGI.""" 

237 # Collect cookies into headers. Have to preserve header case as there 

238 # are some non-RFC compliant clients that require e.g. Content-Type. 

239 response_headers = [] 

240 for header, value in response.items(): 

241 if isinstance(header, str): 

242 header = header.encode("ascii") 

243 if isinstance(value, str): 

244 value = value.encode("latin1") 

245 response_headers.append((bytes(header), bytes(value))) 

246 for c in response.cookies.values(): 

247 response_headers.append( 

248 (b"Set-Cookie", c.output(header="").encode("ascii").strip()) 

249 ) 

250 # Initial response message. 

251 await send( 

252 { 

253 "type": "http.response.start", 

254 "status": response.status_code, 

255 "headers": response_headers, 

256 } 

257 ) 

258 # Streaming responses need to be pinned to their iterator. 

259 if response.streaming: 

260 # Access `__iter__` and not `streaming_content` directly in case 

261 # it has been overridden in a subclass. 

262 for part in response: 

263 for chunk, _ in self.chunk_bytes(part): 

264 await send( 

265 { 

266 "type": "http.response.body", 

267 "body": chunk, 

268 # Ignore "more" as there may be more parts; instead, 

269 # use an empty final closing message with False. 

270 "more_body": True, 

271 } 

272 ) 

273 # Final closing message. 

274 await send({"type": "http.response.body"}) 

275 # Other responses just need chunking. 

276 else: 

277 # Yield chunks of response. 

278 for chunk, last in self.chunk_bytes(response.content): 

279 await send( 

280 { 

281 "type": "http.response.body", 

282 "body": chunk, 

283 "more_body": not last, 

284 } 

285 ) 

286 await sync_to_async(response.close, thread_sensitive=True)() 

287 

288 @classmethod 

289 def chunk_bytes(cls, data): 

290 """ 

291 Chunks some data up so it can be sent in reasonable size messages. 

292 Yields (chunk, last_chunk) tuples. 

293 """ 

294 position = 0 

295 if not data: 

296 yield data, True 

297 return 

298 while position < len(data): 

299 yield ( 

300 data[position : position + cls.chunk_size], 

301 (position + cls.chunk_size) >= len(data), 

302 ) 

303 position += cls.chunk_size 

304 

305 def get_script_prefix(self, scope): 

306 """ 

307 Return the script prefix to use from either the scope or a setting. 

308 """ 

309 if settings.FORCE_SCRIPT_NAME: 

310 return settings.FORCE_SCRIPT_NAME 

311 return scope.get("root_path", "") or ""