def __init__(self): self.asgi: bool = False self.body: Optional[bytes] = None self.content_type: Optional[str] = None self.stream: Http = None self.status: int = None self.headers = Header({}) self._cookies: Optional[CookieJar] = None
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 get_cors_headers(options, request_headers, request_method): origins_to_set = get_cors_origins(options, request_headers.get('Origin')) headers = CIMultiDict() if not origins_to_set: # CORS is not enabled for this route return headers for origin in origins_to_set: # TODO, with CIDict, with will only allow one origin # With CIMultiDict it should work with multiple headers[ACL_ORIGIN] = origin headers[ACL_EXPOSE_HEADERS] = options.get('expose_headers') if options.get('supports_credentials'): headers[ACL_CREDENTIALS] = 'true' # case sensative # This is a preflight request # http://www.w3.org/TR/cors/#resource-preflight-requests if request_method == 'OPTIONS': acl_request_method = request_headers.get(ACL_REQUEST_METHOD, '').upper() # If there is no Access-Control-Request-Method header or if parsing # failed, do not set any additional headers if acl_request_method and acl_request_method in options.get('methods'): # If method is not a case-sensitive match for any of the values in # list of methods do not set any additional headers and terminate # this set of steps. headers[ACL_ALLOW_HEADERS] = get_allow_headers( options, request_headers.get(ACL_REQUEST_HEADERS)) headers[ACL_MAX_AGE] = str(options.get( 'max_age')) # sanic cannot handle integers in header values. headers[ACL_METHODS] = options.get('methods') else: LOG.info( "The request's Access-Control-Request-Method header does not match allowed methods. " "CORS headers will not be applied.") # http://www.w3.org/TR/cors/#resource-implementation if options.get('vary_header'): # Only set header if the origin returned will vary dynamically, # i.e. if we are not returning an asterisk, and there are multiple # origins that can be matched. if headers[ACL_ORIGIN] == '*': pass elif (len(options.get('origins')) > 1 or len(origins_to_set) > 1 or any(map(probably_regex, options.get('origins')))): headers['Vary'] = 'Origin' return CIMultiDict((k, v) for k, v in headers.items() if v)
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 __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
def __init__( self, body=None, status=200, headers=None, content_type=None, ): super().__init__() self.content_type = content_type self.body = self._encode_body(body) self.status = status self.headers = Header(headers or {}) self._cookies = None
def __init__( self, url_bytes: bytes, headers: Header, version: str, method: str, transport: TransportProtocol, app: Sanic, head: bytes = b"", ): self.raw_url = url_bytes try: self._parsed_url = parse_url(url_bytes) except HttpParserInvalidURLError: raise BadURL(f"Bad URL: {url_bytes.decode()}") self._id: Optional[Union[uuid.UUID, str, int]] = None self._name: Optional[str] = None self.app = app self.headers = Header(headers) self.version = version self.method = method self.transport = transport self.head = head # Init but do not inhale self.body = b"" self.conn_info: Optional[ConnInfo] = None self.ctx = SimpleNamespace() self.parsed_forwarded: Optional[Options] = None self.parsed_accept: Optional[AcceptContainer] = None self.parsed_credentials: Optional[Credentials] = None self.parsed_json = None self.parsed_form: Optional[RequestParameters] = None self.parsed_files: Optional[RequestParameters] = None self.parsed_token: Optional[str] = None self.parsed_args: DefaultDict[Tuple[bool, bool, str, str], RequestParameters] = defaultdict( RequestParameters) self.parsed_not_grouped_args: DefaultDict[Tuple[ bool, bool, str, str], List[Tuple[str, str]]] = defaultdict(list) self.request_middleware_started = False self._cookies: Optional[Dict[str, str]] = None self._match_info: Dict[str, Any] = {} self.stream: Optional[Http] = None self.route: Optional[Route] = None self._protocol = None self.responded: bool = False
def __init__( self, streaming_fn: StreamingFunction, status: int = 200, headers: Optional[Union[Header, Dict[str, str]]] = None, content_type: str = "text/plain; charset=utf-8", ignore_deprecation_notice: bool = False, ): if not ignore_deprecation_notice: warn( "Use of the StreamingHTTPResponse is deprecated in v21.6, and " "will be removed in v21.12. Please upgrade your streaming " "response implementation. You can learn more here: " "https://sanicframework.org/en/guide/advanced/streaming.html" "#response-streaming. If you use the builtin stream() or " "file_stream() methods, this upgrade will be be done for you." ) super().__init__() self.content_type = content_type self.streaming_fn = streaming_fn self.status = status self.headers = Header(headers or {}) self._cookies = None
def on_headers_complete(self): self.request = self.request_class( url_bytes=self.url, headers=Header(self.headers), version=self.parser.get_http_version(), method=self.parser.get_method().decode(), transport=self.transport, app=self.app, ) # Remove any existing KeepAlive handler here, # It will be recreated if required on the new request. if self._keep_alive_timeout_handler: self._keep_alive_timeout_handler.cancel() self._keep_alive_timeout_handler = None if self.request.headers.get(EXPECT_HEADER): self.expect_handler() if self.is_request_stream: self._is_stream_handler = self.router.is_stream_handler( self.request) if self._is_stream_handler: self.request.stream = StreamBuffer( self.request_buffer_queue_size) self.execute_request_handler()
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
def test_extract_params(self): with set_request({'QUERY_STRING': b'test=foo&foo=bar'}): uri, http_method, body, headers = extract_params() self.assertEquals(uri, 'http://127.0.0.1/?test=foo&foo=bar') self.assertEquals(http_method, 'GET') self.assertEquals(body, {}) self.assertEquals(headers, Header({'Host': '127.0.0.1'}))
def __init__( self, streaming_fn, status=200, headers=None, content_type="text/plain; charset=utf-8", chunked=True, ): super().__init__() self.content_type = content_type self.streaming_fn = streaming_fn self.status = status self.headers = Header(headers or {}) self.chunked = chunked self._cookies = None self.protocol = None
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.stage = Stage.IDLE instance.response = None 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.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 ) else: raise ServerError("Received unknown ASGI scope") request_class = sanic_app.request_class or Request instance.request = request_class( url_bytes, headers, version, method, instance.transport, sanic_app, ) instance.request.stream = instance instance.request_body = True instance.request.conn_info = ConnInfo(instance.transport) return instance
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 set_request(wsgi_environ, app=None, transport=None): """ Test helper context manager that mocks the sanic request """ environ = {} headers = Header() environ.update(wsgi_environ) for k, v in REQUEST_DEFAULTS.items(): environ.setdefault(k, v) url_bytes = b"%s://%s" % (environ['SCHEME'], environ['HOST']) headers.setdefault('Host', environ['HOST'].decode('utf-8')) if environ['SCHEME'] == b"http" and environ['PORT'] in (80, b"80"): pass elif environ['SCHEME'] == b"https" and environ['PORT'] in (443, b"443"): pass else: port = environ['PORT'] if isinstance(port, int): port = str(port) if isinstance(port, str): port = port.encode('latin-1') url_bytes = b"%s:%s" % (url_bytes, port) if environ['PATH'] == b"": pass else: path = environ['PATH'] if path == b"/": path = b"" url_bytes = b"%s/%s" % (url_bytes, path) if environ['QUERY_STRING'] == b"": pass else: url_bytes = b"%s?%s" % (url_bytes, environ['QUERY_STRING']) version = environ['VERSION'].decode('utf-8') method = environ['METHOD'].decode('utf-8') if app is None: app = set_request.app if transport is None: transport = DummyTransport() r = SanicRequest(url_bytes, headers, version, method, transport, app) with mock.patch.dict(extract_params.__globals__, {'request': r}): yield
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 if body_bytes: warnings.warn( "Parameter `body_bytes` is deprecated, use `body` instead", DeprecationWarning, )
def __init__( self, url_bytes: bytes, headers: Header, version: str, method: str, transport: TransportProtocol, app: Sanic, head: bytes = b"", ): self.raw_url = url_bytes # TODO: Content-Encoding detection self._parsed_url = parse_url(url_bytes) self._id: Optional[Union[uuid.UUID, str, int]] = None self._name: Optional[str] = None self.app = app self.headers = Header(headers) self.version = version self.method = method self.transport = transport self.head = head # Init but do not inhale self.body = b"" self.conn_info: Optional[ConnInfo] = None self.ctx = SimpleNamespace() self.parsed_forwarded: Optional[Options] = None self.parsed_json = None self.parsed_form = None self.parsed_files = None self.parsed_args: DefaultDict[ Tuple[bool, bool, str, str], RequestParameters ] = defaultdict(RequestParameters) self.parsed_not_grouped_args: DefaultDict[ Tuple[bool, bool, str, str], List[Tuple[str, str]] ] = defaultdict(list) self.request_middleware_started = False self._cookies: Optional[Dict[str, str]] = None self._match_info: Dict[str, Any] = {} self.stream: Optional[Http] = None self.route: Optional[Route] = None self._protocol = None
def prepare_slack_request(headers: Dict[Text, Any]) -> Request: request = Request( b"/webhooks/slack/webhook", headers=Header(headers), version="1.1", method="POST", transport=None, app=None, ) request.body = b"""{"foo": "bar"}""" return request
async def test(request: Request): headers = Header() cookies = CookieJar(headers) cookies["test"] = "modified" cookies["test"] = "pass" response = await request.respond(content_type="text/csv", headers=headers) await response.send("foo,") await asyncio.sleep(0.001) await response.send("bar")
def __init__( self, streaming_fn: Callable[[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None], ], status: int = 200, headers: Optional[Union[Header, Dict[str, str]]] = None, content_type: Optional[str] = None, ): self.streaming_fn = streaming_fn self.status = status self.headers = headers or Header() self.content_type = content_type self.request: Optional[Request] = None self._cookies: Optional[CookieJar] = None
def create_empty_request(self): """Current error handling code needs a request object that won't exist if an error occurred during before a request was received. Create a bogus response for error handling use.""" # FIXME: Avoid this by refactoring error handling and response code self.request = self.protocol.request_class( url_bytes=self.url.encode() if self.url else b"*", headers=Header({}), version="1.1", method="NONE", transport=self.protocol.transport, app=self.protocol.app, ) self.request.stream = self
def __init__( self, body: Optional[AnyStr] = None, status: int = 200, headers: Optional[Union[Header, Dict[str, str]]] = None, content_type: Optional[str] = None, ): super().__init__() self.content_type: Optional[str] = content_type self.body = self._encode_body(body) self.status = status self.headers = Header(headers or {}) self._cookies = None
def __init__( self, streaming_fn, status=200, headers=None, content_type="text/plain; charset=utf-8", chunked="deprecated", ): if chunked != "deprecated": warn("The chunked argument has been deprecated and will be " "removed in v21.6") super().__init__() self.content_type = content_type self.streaming_fn = streaming_fn self.status = status self.headers = Header(headers or {}) self._cookies = None
def __init__( self, data=None, status=200, message=None, headers=None, content_type="application/json" ): self.content_type = content_type body = dict(code=status,msg=message, data=data) json_str = json_dumps(body, ensure_ascii=False) self.body = self._encode_body(json_str) self.status = status self.headers = Header(headers or {}) self._cookies = None
def __init__( self, streaming_fn: StreamingFunction, status: int = 200, headers: Optional[Union[Header, Dict[str, str]]] = None, content_type: str = "text/plain; charset=utf-8", chunked="deprecated", ): if chunked != "deprecated": warn("The chunked argument has been deprecated and will be " "removed in v21.6") super().__init__() self.content_type = content_type self.streaming_fn = streaming_fn self.status = status self.headers = Header(headers or {}) self._cookies = None
class Request: """ Properties of an HTTP request such as URL, headers, etc. """ __slots__ = ( "__weakref__", "_cookies", "_id", "_ip", "_parsed_url", "_port", "_protocol", "_remote_addr", "_socket", "_match_info", "_name", "app", "body", "conn_info", "ctx", "head", "headers", "method", "parsed_args", "parsed_not_grouped_args", "parsed_files", "parsed_form", "parsed_json", "parsed_forwarded", "raw_url", "request_middleware_started", "route", "stream", "transport", "version", ) def __init__( self, url_bytes: bytes, headers: Header, version: str, method: str, transport: TransportProtocol, app: Sanic, head: bytes = b"", ): self.raw_url = url_bytes # TODO: Content-Encoding detection self._parsed_url = parse_url(url_bytes) self._id: Optional[Union[uuid.UUID, str, int]] = None self._name: Optional[str] = None self.app = app self.headers = Header(headers) self.version = version self.method = method self.transport = transport self.head = head # Init but do not inhale self.body = b"" self.conn_info: Optional[ConnInfo] = None self.ctx = SimpleNamespace() self.parsed_forwarded: Optional[Options] = None self.parsed_json = None self.parsed_form = None self.parsed_files = None self.parsed_args: DefaultDict[Tuple[bool, bool, str, str], RequestParameters] = defaultdict( RequestParameters) self.parsed_not_grouped_args: DefaultDict[Tuple[ bool, bool, str, str], List[Tuple[str, str]]] = defaultdict(list) self.request_middleware_started = False self._cookies: Optional[Dict[str, str]] = None self._match_info: Dict[str, Any] = {} self.stream: Optional[Http] = None self.route: Optional[Route] = None self._protocol = None def __repr__(self): class_name = self.__class__.__name__ return f"<{class_name}: {self.method} {self.path}>" @classmethod def generate_id(*_): return uuid.uuid4() async def respond( self, response: Optional[BaseHTTPResponse] = None, *, status: int = 200, headers: Optional[Union[Header, Dict[str, str]]] = None, content_type: Optional[str] = None, ): # This logic of determining which response to use is subject to change if response is None: response = (self.stream and self.stream.response) or HTTPResponse( status=status, headers=headers, content_type=content_type, ) # Connect the response if isinstance(response, BaseHTTPResponse) and self.stream: response = self.stream.respond(response) # Run response middleware try: response = await self.app._run_response_middleware( self, response, request_name=self.name) except CancelledErrors: raise except Exception: error_logger.exception( "Exception occurred in one of response middleware handlers") return response async def receive_body(self): """Receive request.body, if not already received. Streaming handlers may call this to receive the full body. Sanic calls this function before running any handlers of non-streaming routes. Custom request classes can override this for custom handling of both streaming and non-streaming routes. """ if not self.body: self.body = b"".join([data async for data in self.stream]) @property def name(self): if self._name: return self._name elif self.route: return self.route.name return None @property def endpoint(self): return self.name @property def uri_template(self): return f"/{self.route.path}" @property def protocol(self): if not self._protocol: self._protocol = self.transport.get_protocol() return self._protocol @property def raw_headers(self): _, headers = self.head.split(b"\r\n", 1) return bytes(headers) @property def request_line(self): reqline, _ = self.head.split(b"\r\n", 1) return bytes(reqline) @property def id(self) -> Optional[Union[uuid.UUID, str, int]]: """ A request ID passed from the client, or generated from the backend. By default, this will look in a request header defined at: ``self.app.config.REQUEST_ID_HEADER``. It defaults to ``X-Request-ID``. Sanic will try to cast the ID into a ``UUID`` or an ``int``. If there is not a UUID from the client, then Sanic will try to generate an ID by calling ``Request.generate_id()``. The default behavior is to generate a ``UUID``. You can customize this behavior by subclassing ``Request``. .. code-block:: python from sanic import Request, Sanic from itertools import count class IntRequest(Request): counter = count() def generate_id(self): return next(self.counter) app = Sanic("MyApp", request_class=IntRequest) """ if not self._id: self._id = self.headers.getone( self.app.config.REQUEST_ID_HEADER, self.__class__.generate_id(self), # type: ignore ) # Try casting to a UUID or an integer if isinstance(self._id, str): try: self._id = uuid.UUID(self._id) except ValueError: try: self._id = int(self._id) # type: ignore except ValueError: ... return self._id # type: ignore @property def json(self): if self.parsed_json is None: self.load_json() return self.parsed_json def load_json(self, loads=json_loads): try: self.parsed_json = loads(self.body) except Exception: if not self.body: return None raise InvalidUsage("Failed when parsing body as json") return self.parsed_json @property def token(self): """Attempt to return the auth header token. :return: token related to request """ prefixes = ("Bearer", "Token") auth_header = self.headers.getone("authorization", None) if auth_header is not None: for prefix in prefixes: if prefix in auth_header: return auth_header.partition(prefix)[-1].strip() return auth_header @property def form(self): if self.parsed_form is None: self.parsed_form = RequestParameters() self.parsed_files = RequestParameters() content_type = self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE) content_type, parameters = parse_content_header(content_type) try: if content_type == "application/x-www-form-urlencoded": self.parsed_form = RequestParameters( parse_qs(self.body.decode("utf-8"))) elif content_type == "multipart/form-data": # TODO: Stream this instead of reading to/from memory boundary = parameters["boundary"].encode("utf-8") self.parsed_form, self.parsed_files = parse_multipart_form( self.body, boundary) except Exception: error_logger.exception("Failed when parsing form") return self.parsed_form @property def files(self): if self.parsed_files is None: self.form # compute form to get files return self.parsed_files def get_args( self, keep_blank_values: bool = False, strict_parsing: bool = False, encoding: str = "utf-8", errors: str = "replace", ) -> RequestParameters: """ Method to parse `query_string` using `urllib.parse.parse_qs`. This methods is used by `args` property. Can be used directly if you need to change default parameters. :param keep_blank_values: flag indicating whether blank values in percent-encoded queries should be treated as blank strings. A true value indicates that blanks should be retained as blank strings. The default false value indicates that blank values are to be ignored and treated as if they were not included. :type keep_blank_values: bool :param strict_parsing: flag indicating what to do with parsing errors. If false (the default), errors are silently ignored. If true, errors raise a ValueError exception. :type strict_parsing: bool :param encoding: specify how to decode percent-encoded sequences into Unicode characters, as accepted by the bytes.decode() method. :type encoding: str :param errors: specify how to decode percent-encoded sequences into Unicode characters, as accepted by the bytes.decode() method. :type errors: str :return: RequestParameters """ if ( keep_blank_values, strict_parsing, encoding, errors, ) not in self.parsed_args: if self.query_string: self.parsed_args[(keep_blank_values, strict_parsing, encoding, errors)] = RequestParameters( parse_qs( qs=self.query_string, keep_blank_values=keep_blank_values, strict_parsing=strict_parsing, encoding=encoding, errors=errors, )) return self.parsed_args[(keep_blank_values, strict_parsing, encoding, errors)] args = property(get_args) def get_query_args( self, keep_blank_values: bool = False, strict_parsing: bool = False, encoding: str = "utf-8", errors: str = "replace", ) -> list: """ Method to parse `query_string` using `urllib.parse.parse_qsl`. This methods is used by `query_args` property. Can be used directly if you need to change default parameters. :param keep_blank_values: flag indicating whether blank values in percent-encoded queries should be treated as blank strings. A true value indicates that blanks should be retained as blank strings. The default false value indicates that blank values are to be ignored and treated as if they were not included. :type keep_blank_values: bool :param strict_parsing: flag indicating what to do with parsing errors. If false (the default), errors are silently ignored. If true, errors raise a ValueError exception. :type strict_parsing: bool :param encoding: specify how to decode percent-encoded sequences into Unicode characters, as accepted by the bytes.decode() method. :type encoding: str :param errors: specify how to decode percent-encoded sequences into Unicode characters, as accepted by the bytes.decode() method. :type errors: str :return: list """ if ( keep_blank_values, strict_parsing, encoding, errors, ) not in self.parsed_not_grouped_args: if self.query_string: self.parsed_not_grouped_args[( keep_blank_values, strict_parsing, encoding, errors)] = parse_qsl( qs=self.query_string, keep_blank_values=keep_blank_values, strict_parsing=strict_parsing, encoding=encoding, errors=errors, ) return self.parsed_not_grouped_args[(keep_blank_values, strict_parsing, encoding, errors)] query_args = property(get_query_args) """ Convenience property to access :meth:`Request.get_query_args` with default values. """ @property def cookies(self) -> Dict[str, str]: """ :return: Incoming cookies on the request :rtype: Dict[str, str] """ if self._cookies is None: cookie = self.headers.getone("cookie", None) if cookie is not None: cookies: SimpleCookie = SimpleCookie() cookies.load(cookie) self._cookies = { name: cookie.value for name, cookie in cookies.items() } else: self._cookies = {} return self._cookies @property def content_type(self) -> str: """ :return: Content-Type header form the request :rtype: str """ return self.headers.getone("content-type", DEFAULT_HTTP_CONTENT_TYPE) @property def match_info(self): """ :return: matched info after resolving route """ return self._match_info # Transport properties (obtained from local interface only) @property def ip(self) -> str: """ :return: peer ip of the socket :rtype: str """ return self.conn_info.client if self.conn_info else "" @property def port(self) -> int: """ :return: peer port of the socket :rtype: int """ return self.conn_info.client_port if self.conn_info else 0 @property def socket(self): return self.conn_info.peername if self.conn_info else (None, None) @property def path(self) -> str: """ :return: path of the local HTTP request :rtype: str """ return self._parsed_url.path.decode("utf-8") # Proxy properties (using SERVER_NAME/forwarded/request/transport info) @property def forwarded(self) -> Options: """ Active proxy information obtained from request headers, as specified in Sanic configuration. Field names by, for, proto, host, port and path are normalized. - for and by IPv6 addresses are bracketed - port (int) is only set by port headers, not from host. - path is url-unencoded Additional values may be available from new style Forwarded headers. :return: forwarded address info :rtype: Dict[str, str] """ if self.parsed_forwarded is None: self.parsed_forwarded = ( parse_forwarded(self.headers, self.app.config) or parse_xforwarded(self.headers, self.app.config) or {}) return self.parsed_forwarded @property def remote_addr(self) -> str: """ Client IP address, if available. 1. proxied remote address `self.forwarded['for']` 2. local remote address `self.ip` :return: IPv4, bracketed IPv6, UNIX socket name or arbitrary string :rtype: str """ if not hasattr(self, "_remote_addr"): self._remote_addr = str(self.forwarded.get("for", "")) # or self.ip return self._remote_addr @property def scheme(self) -> str: """ Determine request scheme. 1. `config.SERVER_NAME` if in full URL format 2. proxied proto/scheme 3. local connection protocol :return: http|https|ws|wss or arbitrary value given by the headers. :rtype: str """ if "//" in self.app.config.get("SERVER_NAME", ""): return self.app.config.SERVER_NAME.split("//")[0] if "proto" in self.forwarded: return str(self.forwarded["proto"]) if (self.app.websocket_enabled and self.headers.getone("upgrade", "").lower() == "websocket"): scheme = "ws" else: scheme = "http" if self.transport.get_extra_info("sslcontext"): scheme += "s" return scheme @property def host(self) -> str: """ The currently effective server 'host' (hostname or hostname:port). 1. `config.SERVER_NAME` overrides any client headers 2. proxied host of original request 3. request host header hostname and port may be separated by `sanic.headers.parse_host(request.host)`. :return: the first matching host found, or empty string :rtype: str """ server_name = self.app.config.get("SERVER_NAME") if server_name: return server_name.split("//", 1)[-1].split("/", 1)[0] return str( self.forwarded.get("host") or self.headers.getone("host", "")) @property def server_name(self) -> str: """ :return: hostname the client connected to, by ``request.host`` :rtype: str """ return parse_host(self.host)[0] or "" @property def server_port(self) -> int: """ The port the client connected to, by forwarded ``port`` or ``request.host``. Default port is returned as 80 and 443 based on ``request.scheme``. :return: port number :rtype: int """ port = self.forwarded.get("port") or parse_host(self.host)[1] return int(port or (80 if self.scheme in ("http", "ws") else 443)) @property def server_path(self) -> str: """ :return: full path of current URL; uses proxied or local path :rtype: str """ return str(self.forwarded.get("path") or self.path) @property def query_string(self) -> str: """ :return: representation of the requested query :rtype: str """ if self._parsed_url.query: return self._parsed_url.query.decode("utf-8") else: return "" @property def url(self) -> str: """ :return: the URL :rtype: str """ return urlunparse( (self.scheme, self.host, self.path, None, self.query_string, None)) def url_for(self, view_name: str, **kwargs) -> str: """ Same as :func:`sanic.Sanic.url_for`, but automatically determine `scheme` and `netloc` base on the request. Since this method is aiming to generate correct schema & netloc, `_external` is implied. :param kwargs: takes same parameters as in :func:`sanic.Sanic.url_for` :return: an absolute url to the given view :rtype: str """ # Full URL SERVER_NAME can only be handled in app.url_for try: if "//" in self.app.config.SERVER_NAME: return self.app.url_for(view_name, _external=True, **kwargs) except AttributeError: pass scheme = self.scheme host = self.server_name port = self.server_port if (scheme.lower() in ("http", "ws") and port == 80) or (scheme.lower() in ("https", "wss") and port == 443): netloc = host else: netloc = f"{host}:{port}" return self.app.url_for(view_name, _external=True, _scheme=scheme, _server=netloc, **kwargs)
class BaseHTTPResponse: """ The base class for all HTTP Responses """ def __init__(self): self.asgi: bool = False self.body: Optional[bytes] = None self.content_type: Optional[str] = None self.stream: Http = None self.status: int = None self.headers = Header({}) self._cookies: Optional[CookieJar] = None def _encode_body(self, data: Optional[AnyStr]): if data is None: return b"" return ( data.encode() if hasattr(data, "encode") else data # type: ignore ) @property def cookies(self) -> CookieJar: """ The response cookies. Cookies should be set and written as follows: .. code-block:: python response.cookies["test"] = "It worked!" response.cookies["test"]["domain"] = ".yummy-yummy-cookie.com" response.cookies["test"]["httponly"] = True `See user guide <https://sanicframework.org/guide/basics/cookies.html>`_ :return: the cookie jar :rtype: CookieJar """ if self._cookies is None: self._cookies = CookieJar(self.headers) return self._cookies @property def processed_headers(self) -> Iterator[Tuple[bytes, bytes]]: """ Obtain a list of header tuples encoded in bytes for sending. Add and remove headers based on status and content_type. :return: response headers :rtype: Tuple[Tuple[bytes, bytes], ...] """ # TODO: Make a blacklist set of header names and then filter with that if self.status in (304, 412): # Not Modified, Precondition Failed self.headers = remove_entity_headers(self.headers) if has_message_body(self.status): self.headers.setdefault("content-type", self.content_type) # Encode headers into bytes return ( (name.encode("ascii"), f"{value}".encode(errors="surrogateescape")) for name, value in self.headers.items() ) async def send( self, data: Optional[Union[AnyStr]] = None, end_stream: Optional[bool] = None, ) -> None: """ Send any pending response headers and the given data as body. :param data: str or bytes to be written :param end_stream: whether to close the stream after this block """ if data is None and end_stream is None: end_stream = True if end_stream and not data and self.stream.send is None: return data = ( data.encode() # type: ignore if hasattr(data, "encode") else data or b"" ) await self.stream.send(data, end_stream=end_stream)
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; charset=utf-8", 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 self.protocol = None async def write(self, data): """Writes a chunk of data to the streaming response. :param data: str or bytes-ish data to be written. """ 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): if self.chunked and version == "1.1": self.headers["Transfer-Encoding"] = "chunked" self.headers.pop("Content-Length", None) return super().get_headers(version, keep_alive, keep_alive_timeout)
def handle_error(self, request, e): """ Error handler for the API transforms a raised exception into a Sanic response, with the appropriate HTTP status code and body. :param request: The Sanic Request object :type request: sanic.request.Request :param e: the raised Exception object :type e: Exception """ context = restplus.get_context_from_spf(self.spf_reg) app = context.app #got_request_exception.send(app._get_current_object(), exception=e) if not isinstance(e, SanicException) and app.config.get( 'PROPAGATE_EXCEPTIONS', False): exc_type, exc_value, tb = sys.exc_info() if exc_value is e: raise else: raise e include_message_in_response = app.config.get("ERROR_INCLUDE_MESSAGE", True) include_code_in_response = app.config.get("ERROR_INCLUDE_CODE", True) default_data = {} headers = Header() for typecheck, handler in self._own_and_child_error_handlers.items(): if isinstance(e, typecheck): result = handler(e) default_data, code, headers = unpack( result, HTTPStatus.INTERNAL_SERVER_ERROR) break else: if isinstance(e, SanicException): sanic_code = code = e.status_code try: status = e.args[0] assert isinstance(status, (str, bytes)) except (AttributeError, LookupError, AssertionError): if sanic_code is 200: status = b'OK' # x is y comparison only works between -5 and 256 elif sanic_code == 404: status = b'Not Found' elif sanic_code == 500: status = b'Internal Server Error' else: status = ALL_STATUS_CODES.get(int(sanic_code)) code = HTTPStatus(sanic_code, None) if status and isinstance(status, bytes): status = status.decode('ascii') if include_message_in_response: default_data = {'message': getattr(e, 'message', status)} elif self._default_error_handler: result = self._default_error_handler(e) default_data, code, headers = unpack( result, HTTPStatus.INTERNAL_SERVER_ERROR) else: code = HTTPStatus.INTERNAL_SERVER_ERROR status = ALL_STATUS_CODES.get(code.value, str(e)) if status and isinstance(status, bytes): status = status.decode('ascii') if include_message_in_response: default_data = { 'message': status, } if include_message_in_response: default_data['message'] = default_data.get('message', str(e)) if include_code_in_response: default_data['code'] = int(code) data = getattr(e, 'data', default_data) fallback_mediatype = None if code >= HTTPStatus.INTERNAL_SERVER_ERROR: exc_info = sys.exc_info() if exc_info[1] is None or exc_info[0] is None: e_type = e.__class__ e_value = e e_traceback = e.__traceback__ else: e_type, e_value, e_traceback = exc_info context.log(logging.ERROR, "Caught Exception: {}".format(str(e_type))) context.log(logging.ERROR, "Detail: {}".format(str(e_value))) tb = traceback.format_tb(e_traceback) tb = "".join(tb) context.log(logging.ERROR, "Traceback:\n{}".format(tb)) elif code == HTTPStatus.NOT_FOUND and app.config.get("ERROR_404_HELP", False) \ and include_message_in_response: data['message'] = self._help_on_404(request, data.get('message', None)) elif code == HTTPStatus.NOT_ACCEPTABLE and self.default_mediatype is None: # if we are handling NotAcceptable (406), make sure that # make_response uses a representation we support as the # default mediatype (so that make_response doesn't throw # another NotAcceptable error). supported_mediatypes = list(self.representations.keys()) fallback_mediatype = supported_mediatypes[ 0] if supported_mediatypes else "text/plain" # Remove blacklisted headers for header in HEADERS_BLACKLIST: headers.pop(header, None) resp = self.make_response(request, data, code, headers, fallback_mediatype=fallback_mediatype) if code == HTTPStatus.UNAUTHORIZED: resp = self.unauthorized(resp) return resp
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