class HTTPResponse(BaseHTTPResponse): __slots__ = ("body", "status", "content_type", "headers", "_cookies") def __init__( self, body=None, status=200, headers=None, content_type=None, body_bytes=b"", ): self.content_type = content_type self.body = body_bytes if body is None else self._encode_body(body) self.status = status self.headers = Header(headers or {}) self._cookies = None def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): body = b"" if has_message_body(self.status): body = self.body self.headers["Content-Length"] = self.headers.get( "Content-Length", len(self.body)) return self.get_headers(version, keep_alive, keep_alive_timeout, body) @property def cookies(self): if self._cookies is None: self._cookies = CookieJar(self.headers) return self._cookies
async def create(cls, sanic_app, scope: ASGIScope, receive: ASGIReceive, send: ASGISend) -> "ASGIApp": instance = cls() instance.sanic_app = sanic_app instance.transport = MockTransport(scope, receive, send) instance.transport.loop = sanic_app.loop setattr(instance.transport, "add_task", sanic_app.loop.create_task) headers = Header([(key.decode("latin-1"), value.decode("latin-1")) for key, value in scope.get("headers", [])]) instance.do_stream = (True if headers.get("expect") == "100-continue" else False) instance.lifespan = Lifespan(instance) if scope["type"] == "lifespan": await instance.lifespan(scope, receive, send) else: path = (scope["path"][1:] if scope["path"].startswith("/") else scope["path"]) url = "/".join([scope.get("root_path", ""), quote(path)]) url_bytes = url.encode("latin-1") url_bytes += b"?" + scope["query_string"] if scope["type"] == "http": version = scope["http_version"] method = scope["method"] elif scope["type"] == "websocket": version = "1.1" method = "GET" instance.ws = instance.transport.create_websocket_connection( send, receive) await instance.ws.accept() else: pass # TODO: # - close connection request_class = sanic_app.request_class or Request instance.request = request_class( url_bytes, headers, version, method, instance.transport, sanic_app, ) instance.request.conn_info = ConnInfo(instance.transport) if sanic_app.is_request_stream: is_stream_handler = sanic_app.router.is_stream_handler( instance.request) if is_stream_handler: instance.request.stream = StreamBuffer( sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE) instance.do_stream = True return instance
def make_mocked_request(method: str, path: bytes, headers=None, *args, version=(1, 1), closing=False, app=None, writer=sentinel, protocol=sentinel, transport=sentinel, payload=sentinel, sslcontext=None, client_max_size=1024**2, loop=None, stream=False): """Creates mocked web.Request testing purposes. Useful in unit tests, when spinning full web server is overkill or specific conditions and errors are hard to trigger. """ task = mock.Mock() if loop is None: loop = mock.Mock() loop.create_future.return_value = () if version < (1, 1): closing = True if headers: headers = MultiDict(headers) raw_hdrs = tuple( (k.encode('utf-8'), v.encode('utf-8')) for k, v in headers.items()) else: headers = MultiDict() raw_hdrs = () chunked = 'chunked' in headers.get('Transfer-Encoding', '').lower() # message = RawRequestMessage( # method, path, version, headers, # raw_hdrs, closing, False, False, chunked, URL(path)) if app is None: app = _create_app_mock() if transport is sentinel: transport = _create_transport(sslcontext) if protocol is sentinel: protocol = mock.Mock() protocol.transport = transport if writer is sentinel: writer = mock.Mock() writer.write_headers = make_mocked_coro(None) writer.write = make_mocked_coro(None) writer.write_eof = make_mocked_coro(None) writer.drain = make_mocked_coro(None) writer.transport = transport protocol.transport = transport protocol.writer = writer if payload is sentinel: payload = b"" if sanic_19_6 > sanic_version: req = Request(path, headers, version, method, transport=transport) req.app = app else: req = Request(path, headers, version, method, transport=transport, app=app) if stream: mock_stream = mock.Mock() mock_stream.read = make_mocked_coro(payload) req.stream = mock_stream else: req.body_push(payload) req.body_finish() # req = Request(message, payload, # protocol, writer, task, loop, # client_max_size=client_max_size) return req
class StreamingHTTPResponse(BaseHTTPResponse): __slots__ = ( "protocol", "streaming_fn", "status", "content_type", "headers", "chunked", "_cookies", ) def __init__( self, streaming_fn, status=200, headers=None, content_type="text/plain", chunked=True, ): self.content_type = content_type self.streaming_fn = streaming_fn self.status = status self.headers = Header(headers or {}) self.chunked = chunked self._cookies = None async def write(self, data): """Writes a chunk of data to the streaming response. :param data: bytes-ish data to be written. """ if type(data) != bytes: data = self._encode_body(data) if self.chunked: await self.protocol.push_data(b"%x\r\n%b\r\n" % (len(data), data)) else: await self.protocol.push_data(data) await self.protocol.drain() async def stream(self, version="1.1", keep_alive=False, keep_alive_timeout=None): """Streams headers, runs the `streaming_fn` callback that writes content to the response body, then finalizes the response body. """ if version != "1.1": self.chunked = False headers = self.get_headers( version, keep_alive=keep_alive, keep_alive_timeout=keep_alive_timeout, ) await self.protocol.push_data(headers) await self.protocol.drain() await self.streaming_fn(self) if self.chunked: await self.protocol.push_data(b"0\r\n\r\n") # no need to await drain here after this write, because it is the # very last thing we write and nothing needs to wait for it. def get_headers(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python timeout_header = b"" if keep_alive and keep_alive_timeout is not None: timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout if self.chunked and version == "1.1": self.headers["Transfer-Encoding"] = "chunked" self.headers.pop("Content-Length", None) self.headers["Content-Type"] = self.headers.get( "Content-Type", self.content_type) headers = self._parse_headers() if self.status == 200: status = b"OK" else: status = STATUS_CODES.get(self.status) return (b"HTTP/%b %d %b\r\n" b"%b" b"%b\r\n") % ( version.encode(), self.status, status, timeout_header, headers, )
class HTTPResponse(BaseHTTPResponse): __slots__ = ("body", "status", "content_type", "headers", "_cookies") def __init__( self, body=None, status=200, headers=None, content_type="text/plain", body_bytes=b"", ): self.content_type = content_type if body is not None: self.body = self._encode_body(body) else: self.body = body_bytes self.status = status self.headers = Header(headers or {}) self._cookies = None def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): # This is all returned in a kind-of funky way # We tried to make this as fast as possible in pure python timeout_header = b"" if keep_alive and keep_alive_timeout is not None: timeout_header = b"Keep-Alive: %d\r\n" % keep_alive_timeout body = b"" if has_message_body(self.status): body = self.body self.headers["Content-Length"] = self.headers.get( "Content-Length", len(self.body)) self.headers["Content-Type"] = self.headers.get( "Content-Type", self.content_type) if self.status in (304, 412): self.headers = remove_entity_headers(self.headers) headers = self._parse_headers() if self.status == 200: status = b"OK" else: status = STATUS_CODES.get(self.status, b"UNKNOWN RESPONSE") return (b"HTTP/%b %d %b\r\n" b"Connection: %b\r\n" b"%b" b"%b\r\n" b"%b") % ( version.encode(), self.status, status, b"keep-alive" if keep_alive else b"close", timeout_header, headers, body, ) @property def cookies(self): if self._cookies is None: self._cookies = CookieJar(self.headers) return self._cookies
async def http1_request_header(self): """ Receive and parse request header into self.request. """ HEADER_MAX_SIZE = min(8192, self.request_max_size) # Receive until full header is in buffer buf = self.recv_buffer pos = 0 while True: pos = buf.find(b"\r\n\r\n", pos) if pos != -1: break pos = max(0, len(buf) - 3) if pos >= HEADER_MAX_SIZE: break await self._receive_more() if pos >= HEADER_MAX_SIZE: raise PayloadTooLarge("Request header exceeds the size limit") # Parse header content try: head = buf[:pos] raw_headers = head.decode(errors="surrogateescape") reqline, *split_headers = raw_headers.split("\r\n") method, self.url, protocol = reqline.split(" ") if protocol == "HTTP/1.1": self.keep_alive = True elif protocol == "HTTP/1.0": self.keep_alive = False else: raise Exception # Raise a Bad Request on try-except self.head_only = method.upper() == "HEAD" request_body = False headers = [] for name, value in (h.split(":", 1) for h in split_headers): name, value = h = name.lower(), value.lstrip() if name in ("content-length", "transfer-encoding"): request_body = True elif name == "connection": self.keep_alive = value.lower() == "keep-alive" headers.append(h) except Exception: raise InvalidUsage("Bad Request") headers_instance = Header(headers) self.upgrade_websocket = (headers_instance.get( "upgrade", "").lower() == "websocket") # Prepare a Request object request = self.protocol.request_class( url_bytes=self.url.encode(), headers=headers_instance, head=bytes(head), version=protocol[5:], method=method, transport=self.protocol.transport, app=self.protocol.app, ) # Prepare for request body self.request_bytes_left = self.request_bytes = 0 if request_body: headers = request.headers expect = headers.get("expect") if expect is not None: if expect.lower() == "100-continue": self.expecting_continue = True else: raise HeaderExpectationFailed(f"Unknown Expect: {expect}") if headers.get("transfer-encoding") == "chunked": self.request_body = "chunked" pos -= 2 # One CRLF stays in buffer else: self.request_body = True self.request_bytes_left = self.request_bytes = int( headers["content-length"]) # Remove header and its trailing CRLF del buf[:pos + 4] self.stage = Stage.HANDLER self.request, request.stream = request, self self.protocol.state["requests_count"] += 1
async def create(cls, sanic_app, scope: ASGIScope, receive: ASGIReceive, send: ASGISend) -> "ASGIApp": instance = cls() instance.sanic_app = sanic_app instance.transport = MockTransport(scope, receive, send) instance.transport.loop = sanic_app.loop # 给instance.transport添加add_task并赋值为sanic_app.loop.create_task setattr(instance.transport, "add_task", sanic_app.loop.create_task) headers = Header([ # 默认编码是latin-1(西欧语言) (key.decode("latin-1"), value.decode("latin-1")) for key, value in scope.get("headers", []) ]) instance.do_stream = (True if headers.get("expect") == "100-continue" else False) # 检查实例的before \ after server stop function 是否已经被listen, # 如果已经被listen则warn,因为 # not before the ASGI server is started。 # not after the ASGI server is stopped. instance.lifespan = Lifespan(instance) if scope["type"] == "lifespan": await instance.lifespan(scope, receive, send) else: path = ( # 只包含不带/的path scope["path"][1:] if scope["path"].startswith("/") else scope["path"]) # quote去掉特殊字符,Replace special characters in string using the %xx escape. url = "/".join([scope.get("root_path", ""), quote(path)]) url_bytes = url.encode("latin-1") # utf-8 url_bytes += b"?" + scope["query_string"] if scope["type"] == "http": version = scope["http_version"] method = scope["method"] elif scope["type"] == "websocket": # 设置websocket 的http 版本为1.1 get version = "1.1" method = "GET" instance.ws = instance.transport.create_websocket_connection( send, receive) await instance.ws.accept() else: pass # TODO: # - close connection request_class = sanic_app.request_class or Request instance.request = request_class( url_bytes, headers, version, method, instance.transport, sanic_app, ) if sanic_app.is_request_stream: is_stream_handler = sanic_app.router.is_stream_handler( instance.request) if is_stream_handler: # StreamBuffer其实就是 asyncio.queue instance.request.stream = StreamBuffer( sanic_app.config.REQUEST_BUFFER_QUEUE_SIZE) instance.do_stream = True return instance